@open-agent-toolkit/cli 0.3.38 → 0.3.39

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 (61) hide show
  1. package/README.md +2 -2
  2. package/assets/bundle-metadata.json +1 -1
  3. package/assets/docs/getting-started/bootstrap.md +15 -16
  4. package/assets/docs/getting-started/tool-packs.md +15 -14
  5. package/assets/docs/provider-sync/commands.md +6 -5
  6. package/assets/docs/provider-sync/manifest-and-drift.md +26 -19
  7. package/assets/docs/provider-sync/pilot-with-a-team.md +17 -17
  8. package/assets/docs/provider-sync/providers.md +4 -4
  9. package/assets/docs/provider-sync/scope-and-surface.md +5 -5
  10. package/assets/docs/reference/cli-reference.md +5 -2
  11. package/assets/docs/reference/oat-directory-structure.md +1 -1
  12. package/assets/docs/reference/troubleshooting.md +4 -5
  13. package/assets/docs/reference/what-oat-writes.md +54 -59
  14. package/assets/docs/workflows/projects/closeout/closeout-skills.md +6 -5
  15. package/assets/docs/workflows/projects/execution/execution-skills.md +1 -2
  16. package/assets/docs/workflows/projects/execution/implementation-execution.md +3 -2
  17. package/assets/public-package-versions.json +4 -4
  18. package/assets/skills/oat-project-complete/SKILL.md +17 -7
  19. package/assets/skills/oat-project-implement/SKILL.md +2 -2
  20. package/assets/skills/oat-project-pr-final/SKILL.md +2 -2
  21. package/dist/commands/doctor/index.d.ts.map +1 -1
  22. package/dist/commands/doctor/index.js +5 -2
  23. package/dist/commands/init/index.d.ts.map +1 -1
  24. package/dist/commands/init/index.js +10 -5
  25. package/dist/commands/init/tools/index.d.ts.map +1 -1
  26. package/dist/commands/init/tools/index.js +106 -28
  27. package/dist/commands/init/tools/project-guidance.js +1 -1
  28. package/dist/commands/project/new/index.d.ts.map +1 -1
  29. package/dist/commands/project/new/index.js +1 -0
  30. package/dist/commands/project/new/scaffold.d.ts +1 -1
  31. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  32. package/dist/commands/project/new/scaffold.js +1 -1
  33. package/dist/commands/providers/codex/materialize.d.ts +5 -1
  34. package/dist/commands/providers/codex/materialize.d.ts.map +1 -1
  35. package/dist/commands/providers/codex/materialize.js +6 -1
  36. package/dist/commands/shared/exact-path-commit.d.ts.map +1 -1
  37. package/dist/commands/shared/exact-path-commit.js +11 -1
  38. package/dist/commands/shared/scope-option.d.ts +3 -1
  39. package/dist/commands/shared/scope-option.d.ts.map +1 -1
  40. package/dist/commands/shared/scope-option.js +4 -2
  41. package/dist/commands/status/index.d.ts.map +1 -1
  42. package/dist/commands/status/index.js +4 -1
  43. package/dist/commands/tools/install/index.d.ts.map +1 -1
  44. package/dist/commands/tools/install/index.js +1 -1
  45. package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts +11 -0
  46. package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts.map +1 -1
  47. package/dist/commands/tools/shared/pack-lifecycle-outcome.js +16 -0
  48. package/dist/config/oat-config.d.ts +6 -1
  49. package/dist/config/oat-config.d.ts.map +1 -1
  50. package/dist/config/oat-config.js +14 -22
  51. package/dist/index.d.ts +6 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +16 -0
  54. package/dist/providers/codex/codec/config-merge.d.ts +19 -0
  55. package/dist/providers/codex/codec/config-merge.d.ts.map +1 -1
  56. package/dist/providers/codex/codec/config-merge.js +75 -8
  57. package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
  58. package/dist/providers/codex/codec/sync-extension.js +84 -38
  59. package/dist/providers/cursor/codec/materialize.d.ts.map +1 -1
  60. package/dist/providers/cursor/codec/materialize.js +17 -1
  61. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { execSync } from 'node:child_process';
1
+ import { execFileSync } from 'node:child_process';
2
2
  import { lstat, readFile } from 'node:fs/promises';
3
3
  import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { CliError } from '../errors/cli-error.js';
@@ -1726,29 +1726,21 @@ export async function clearActiveIdea(repoRoot) {
1726
1726
  activeIdea: null,
1727
1727
  });
1728
1728
  }
1729
- export function detectDefaultBranch(repoRoot) {
1730
- try {
1731
- const branch = execSync('gh repo view --json defaultBranchRef --jq .defaultBranchRef.name', {
1732
- cwd: repoRoot,
1733
- encoding: 'utf8',
1734
- timeout: 10_000,
1735
- stdio: ['pipe', 'pipe', 'pipe'],
1736
- }).trim();
1737
- if (branch)
1738
- return branch;
1739
- }
1740
- catch {
1741
- // gh not available or not authenticated — fall through
1742
- }
1729
+ const runGitCommand = (args, cwd) => execFileSync('git', args, {
1730
+ cwd,
1731
+ encoding: 'utf8',
1732
+ timeout: 5_000,
1733
+ stdio: ['pipe', 'pipe', 'pipe'],
1734
+ });
1735
+ /**
1736
+ * Local-only default-branch detection: `origin/HEAD`, then `main`. It runs
1737
+ * only `git`, so it never contacts a remote service or runs `gh`.
1738
+ */
1739
+ export function detectDefaultBranch(repoRoot, runGit = runGitCommand) {
1743
1740
  try {
1744
- const ref = execSync('git rev-parse --abbrev-ref origin/HEAD', {
1745
- cwd: repoRoot,
1746
- encoding: 'utf8',
1747
- timeout: 5_000,
1748
- stdio: ['pipe', 'pipe', 'pipe'],
1749
- }).trim();
1741
+ const ref = runGit(['rev-parse', '--abbrev-ref', 'origin/HEAD'], repoRoot).trim();
1750
1742
  if (ref.startsWith('origin/'))
1751
- return ref.replace('origin/', '');
1743
+ return ref.slice('origin/'.length);
1752
1744
  }
1753
1745
  catch {
1754
1746
  // origin/HEAD not set — fall through
package/dist/index.d.ts CHANGED
@@ -2,4 +2,10 @@
2
2
  export declare function normalizeArgv(argv: string[]): string[];
3
3
  export declare function main(argv?: string[], platform?: NodeJS.Platform): Promise<void>;
4
4
  export declare function isEntrypoint(argv?: string[], entrypointUrl?: string): boolean;
5
+ /**
6
+ * A reader that hangs up early (`oat tools list | head -1`) has received all
7
+ * the output it wanted, so a broken pipe ends the process quietly. Any other
8
+ * stream error is rethrown and stays fatal.
9
+ */
10
+ export declare function exitQuietlyOnBrokenPipe(stream: NodeJS.WriteStream, exit?: (code: number) => void): void;
5
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAuBA,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAatD;AAED,wBAAsB,IAAI,CACxB,IAAI,GAAE,MAAM,EAAiB,EAC7B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED,wBAAgB,YAAY,CAC1B,IAAI,GAAE,MAAM,EAAiB,EAC7B,aAAa,GAAE,MAAwB,GACtC,OAAO,CAaT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAuBA,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAatD;AAED,wBAAsB,IAAI,CACxB,IAAI,GAAE,MAAM,EAAiB,EAC7B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED,wBAAgB,YAAY,CAC1B,IAAI,GAAE,MAAM,EAAiB,EAC7B,aAAa,GAAE,MAAwB,GACtC,OAAO,CAaT;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,WAAW,EAC1B,IAAI,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAmC,GAC1D,IAAI,CAQN"}
package/dist/index.js CHANGED
@@ -81,7 +81,23 @@ export function isEntrypoint(argv = process.argv, entrypointUrl = import.meta.ur
81
81
  return entrypointUrl === pathToFileURL(argvPath).href;
82
82
  }
83
83
  }
84
+ /**
85
+ * A reader that hangs up early (`oat tools list | head -1`) has received all
86
+ * the output it wanted, so a broken pipe ends the process quietly. Any other
87
+ * stream error is rethrown and stays fatal.
88
+ */
89
+ export function exitQuietlyOnBrokenPipe(stream, exit = (code) => process.exit(code)) {
90
+ stream.on('error', (error) => {
91
+ if (error.code === 'EPIPE') {
92
+ exit(0);
93
+ return;
94
+ }
95
+ throw error;
96
+ });
97
+ }
84
98
  if (isEntrypoint()) {
99
+ exitQuietlyOnBrokenPipe(process.stdout);
100
+ exitQuietlyOnBrokenPipe(process.stderr);
85
101
  void main().catch((error) => {
86
102
  const logger = createLogger({ json: false, verbose: false });
87
103
  if (error instanceof CliError) {
@@ -15,11 +15,30 @@ export interface CodexSingleRoleConfigMergeArgs {
15
15
  staleManagedRoles?: string[];
16
16
  inheritedMaxDepth?: number;
17
17
  }
18
+ export type CodexConfigUserKey = 'features.multi_agent' | 'agents.max_depth';
19
+ export interface CodexConfigAddedKey {
20
+ key: CodexConfigUserKey;
21
+ value: boolean | number;
22
+ }
23
+ /** An explicit user value that sync kept although OAT would choose another. */
24
+ export interface CodexConfigUserValueNotice {
25
+ key: CodexConfigUserKey;
26
+ kind: 'conflict' | 'note';
27
+ value: unknown;
28
+ message: string;
29
+ }
18
30
  export interface CodexConfigMergeResult {
19
31
  mergedContent: string;
20
32
  changed: boolean;
21
33
  removedRoles: string[];
34
+ addedKeys: CodexConfigAddedKey[];
35
+ userValueNotices: CodexConfigUserValueNotice[];
22
36
  }
37
+ /**
38
+ * One line per key sync added and per explicit user value it kept against
39
+ * OAT's preference, for command output.
40
+ */
41
+ export declare function describeCodexConfigMerge(result: Pick<CodexConfigMergeResult, 'addedKeys' | 'userValueNotices'>): string[];
23
42
  /** Reads a valid numeric agents.max_depth value from Codex TOML content. */
24
43
  export declare function readCodexMaxDepth(content: string | null): number | null;
25
44
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config-merge.d.ts","sourceRoot":"","sources":["../../../../src/providers/codex/codec/config-merge.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,sBAAsB,EAAE,CAAC;IACvC,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,sBAAsB,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAqFD,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAIvE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,eAAe,EACf,YAAY,EACZ,iBAAsB,EACtB,iBAAiB,GAClB,EAAE,oBAAoB,GAAG,sBAAsB,CAkH/C;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,EACtC,eAAe,EACf,IAAI,EACJ,iBAAsB,EACtB,iBAAiB,GAClB,EAAE,8BAA8B,GAAG,sBAAsB,CAOzD"}
1
+ {"version":3,"file":"config-merge.d.ts","sourceRoot":"","sources":["../../../../src/providers/codex/codec/config-merge.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,sBAAsB,EAAE,CAAC;IACvC,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,sBAAsB,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,kBAAkB,GAAG,sBAAsB,GAAG,kBAAkB,CAAC;AAE7E,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,kBAAkB,CAAC;IACxB,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;CACzB;AAED,+EAA+E;AAC/E,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,kBAAkB,CAAC;IACxB,IAAI,EAAE,UAAU,GAAG,MAAM,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,gBAAgB,EAAE,0BAA0B,EAAE,CAAC;CAChD;AA6HD;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,IAAI,CAAC,sBAAsB,EAAE,WAAW,GAAG,kBAAkB,CAAC,GACrE,MAAM,EAAE,CAUV;AAED,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAIvE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,eAAe,EACf,YAAY,EACZ,iBAAsB,EACtB,iBAAiB,GAClB,EAAE,oBAAoB,GAAG,sBAAsB,CA0H/C;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,EACtC,eAAe,EACf,IAAI,EACJ,iBAAsB,EACtB,iBAAiB,GAClB,EAAE,8BAA8B,GAAG,sBAAsB,CAOzD"}
@@ -65,6 +65,53 @@ function canonicalTomlValue(value) {
65
65
  }
66
66
  return value;
67
67
  }
68
+ function renderNoticeValue(value) {
69
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
70
+ }
71
+ function multiAgentNotice(value) {
72
+ if (value === undefined || value === true) {
73
+ return null;
74
+ }
75
+ return {
76
+ key: 'features.multi_agent',
77
+ kind: 'conflict',
78
+ value,
79
+ message: 'OAT-managed Codex roles need true',
80
+ };
81
+ }
82
+ function maxDepthNotice(value) {
83
+ if (value === undefined) {
84
+ return null;
85
+ }
86
+ const depth = getFiniteNumber(value);
87
+ if (depth === null || depth < 1) {
88
+ return {
89
+ key: 'agents.max_depth',
90
+ kind: 'conflict',
91
+ value,
92
+ message: 'default OAT dispatch needs at least 1',
93
+ };
94
+ }
95
+ if (depth < 2) {
96
+ return {
97
+ key: 'agents.max_depth',
98
+ kind: 'note',
99
+ value,
100
+ message: 'this disables only optional nested work',
101
+ };
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * One line per key sync added and per explicit user value it kept against
107
+ * OAT's preference, for command output.
108
+ */
109
+ export function describeCodexConfigMerge(result) {
110
+ return [
111
+ ...result.addedKeys.map(({ key, value }) => `added ${key} = ${renderNoticeValue(value)}`),
112
+ ...result.userValueNotices.map(({ key, kind, value, message }) => `${kind}: kept ${key} = ${renderNoticeValue(value)} (${message})`),
113
+ ];
114
+ }
68
115
  /** Reads a valid numeric agents.max_depth value from Codex TOML content. */
69
116
  export function readCodexMaxDepth(content) {
70
117
  const parsed = parseConfig(content);
@@ -94,8 +141,29 @@ export function mergeCodexConfig({ existingContent, desiredRoles, staleManagedRo
94
141
  requireNotTable(role.description, `agents.${roleName}.description`);
95
142
  requireNotTable(role.config_file, `agents.${roleName}.config_file`);
96
143
  }
97
- const maxDepth = Math.max(2, getFiniteNumber(agents.max_depth) ?? 2, getFiniteNumber(inheritedMaxDepth) ?? 2);
98
- const expectedAgents = { ...agents, max_depth: maxDepth };
144
+ // Explicit user values are never rewritten; only absent keys are added.
145
+ const addedKeys = [];
146
+ const userValueNotices = [];
147
+ const expectedFeatures = { ...features };
148
+ if (features.multi_agent === undefined) {
149
+ expectedFeatures.multi_agent = true;
150
+ addedKeys.push({ key: 'features.multi_agent', value: true });
151
+ }
152
+ const expectedAgents = { ...agents };
153
+ const manageMaxDepth = !stale.has('max_depth');
154
+ if (manageMaxDepth && agents.max_depth === undefined) {
155
+ const maxDepth = Math.max(2, getFiniteNumber(inheritedMaxDepth) ?? 2);
156
+ expectedAgents.max_depth = maxDepth;
157
+ addedKeys.push({ key: 'agents.max_depth', value: maxDepth });
158
+ }
159
+ for (const notice of [
160
+ multiAgentNotice(features.multi_agent),
161
+ manageMaxDepth ? maxDepthNotice(agents.max_depth) : null,
162
+ ]) {
163
+ if (notice) {
164
+ userValueNotices.push(notice);
165
+ }
166
+ }
99
167
  for (const role of roles.values()) {
100
168
  expectedAgents[role.roleName] = {
101
169
  ...getObject(agents[role.roleName]),
@@ -115,11 +183,8 @@ export function mergeCodexConfig({ existingContent, desiredRoles, staleManagedRo
115
183
  ? existingContent
116
184
  : '';
117
185
  let mergedContent = original;
118
- if (features.multi_agent !== true) {
119
- mergedContent = setTomlValue(mergedContent, ['features', 'multi_agent'], 'true');
120
- }
121
- if (agents.max_depth !== maxDepth && !removed.has('max_depth')) {
122
- mergedContent = setTomlValue(mergedContent, ['agents', 'max_depth'], String(maxDepth));
186
+ for (const { key, value } of addedKeys) {
187
+ mergedContent = setTomlValue(mergedContent, key.split('.'), String(value));
123
188
  }
124
189
  for (const staleRole of removedRoles) {
125
190
  mergedContent = removeTomlTable(mergedContent, ['agents', staleRole]);
@@ -138,7 +203,7 @@ export function mergeCodexConfig({ existingContent, desiredRoles, staleManagedRo
138
203
  }
139
204
  const expected = {
140
205
  ...parsed,
141
- features: { ...features, multi_agent: true },
206
+ features: expectedFeatures,
142
207
  agents: expectedAgents,
143
208
  };
144
209
  let actual;
@@ -157,6 +222,8 @@ export function mergeCodexConfig({ existingContent, desiredRoles, staleManagedRo
157
222
  mergedContent,
158
223
  changed: mergedContent !== (existingContent ?? ''),
159
224
  removedRoles,
225
+ addedKeys,
226
+ userValueNotices,
160
227
  };
161
228
  }
162
229
  /** Merges one managed Codex role using the shared configuration contract. */
@@ -1 +1 @@
1
- {"version":3,"file":"sync-extension.d.ts","sourceRoot":"","sources":["../../../../src/providers/codex/codec/sync-extension.ts"],"names":[],"mappings":"AAkBA,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AAKvB,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,6BAA6B,EACnC,MAAM,mBAAmB,CAAC;AAsB3B,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAC;AACzD,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAErD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,4BAA6B,SAAQ,6BAA6B,CACjF,OAAO,EACP,oBAAoB,CACrB;IACC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB,CAC7D,OAAO,EACP,oBAAoB,EACpB,0BAA0B,CAC3B;IACC,UAAU,EAAE,4BAA4B,EAAE,CAAC;IAC3C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,yBAAyB,GAAG,0BAA0B,CAAC;AAiBnE,MAAM,WAAW,iCAAiC;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAoiBD,wBAAsB,gCAAgC,CACpD,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,cAAc,EAAE,EAClC,qBAAqB,CAAC,EAAE,MAAM,EAAE,EAChC,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,kBAAkB,CAAC,CAkL7B;AAED,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,yBAAyB,CAAC,CA+DpC;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAE1E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,kBAAkB,GAAG;IACjE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB,CAEA;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,kBAAkB,GACvB,uBAAuB,EAAE,CAQ3B;AAED,eAAO,MAAM,6BAA6B,EAAE,wBAAwB,CAClE,kBAAkB,EAClB,sBAAsB,CAAC,iCAAiC,CAAC,CAa1D,CAAC"}
1
+ {"version":3,"file":"sync-extension.d.ts","sourceRoot":"","sources":["../../../../src/providers/codex/codec/sync-extension.ts"],"names":[],"mappings":"AAkBA,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AAKvB,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,6BAA6B,EACnC,MAAM,mBAAmB,CAAC;AAwB3B,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAC;AACzD,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAErD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,4BAA6B,SAAQ,6BAA6B,CACjF,OAAO,EACP,oBAAoB,CACrB;IACC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB,CAC7D,OAAO,EACP,oBAAoB,EACpB,0BAA0B,CAC3B;IACC,UAAU,EAAE,4BAA4B,EAAE,CAAC;IAC3C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,yBAAyB,GAAG,0BAA0B,CAAC;AAiBnE,MAAM,WAAW,iCAAiC;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AA4mBD,wBAAsB,gCAAgC,CACpD,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,cAAc,EAAE,EAClC,qBAAqB,CAAC,EAAE,MAAM,EAAE,EAChC,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,kBAAkB,CAAC,CAoN7B;AAED,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,yBAAyB,CAAC,CA+DpC;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAE1E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,kBAAkB,GAAG;IACjE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB,CAEA;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,kBAAkB,GACvB,uBAAuB,EAAE,CAQ3B;AAED,eAAO,MAAM,6BAA6B,EAAE,wBAAwB,CAClE,kBAAkB,EAClB,sBAAsB,CAAC,iCAAiC,CAAC,CAa1D,CAAC"}
@@ -12,7 +12,7 @@ import TOML from '@iarna/toml';
12
12
  import { hasMaterializationChanges, summarizeMaterializationPlan, } from '../../shared/index.js';
13
13
  import YAML from 'yaml';
14
14
  import { SUPPORTED_CODEX_BASE_ROLES, SUPPORTED_CODEX_ROLE_TARGETS, } from './catalog.js';
15
- import { mergeCodexConfig, readCodexMaxDepth, } from './config-merge.js';
15
+ import { describeCodexConfigMerge, mergeCodexConfig, readCodexMaxDepth, } from './config-merge.js';
16
16
  import { exportCanonicalAgentToCodexRole } from './export-to-codex.js';
17
17
  import { materializeCodexRole } from './materialize.js';
18
18
  import { isOatManagedCodexRoleFile, readOatManagedCodexRoleOwner, withOatManagedCodexRoleOwner, } from './shared.js';
@@ -35,6 +35,13 @@ async function readOptionalFile(path) {
35
35
  }
36
36
  return readFile(path, 'utf8');
37
37
  }
38
+ function describeConfigOperation(summary, configMerge) {
39
+ const details = describeCodexConfigMerge(configMerge);
40
+ if (configMerge.removedRoles.length > 0) {
41
+ details.unshift(`removes stale role tables: ${configMerge.removedRoles.map((role) => `agents.${role}`).join(', ')}`);
42
+ }
43
+ return [summary, ...details].join('; ');
44
+ }
38
45
  function normalizeManagedRolesConfig(desiredRoles) {
39
46
  return desiredRoles.map((role) => ({
40
47
  roleName: role.roleName,
@@ -42,7 +49,7 @@ function normalizeManagedRolesConfig(desiredRoles) {
42
49
  configFile: role.configFile,
43
50
  }));
44
51
  }
45
- async function desiredRolesFromCanonical(canonicalEntries, scopeRoot, materializationTargets) {
52
+ async function desiredRolesFromCanonical(canonicalEntries, scopeRoot, materializationTargets, scopeOwner) {
46
53
  const roles = [];
47
54
  for (const entry of canonicalEntries) {
48
55
  if (entry.type !== 'agent' ||
@@ -57,7 +64,7 @@ async function desiredRolesFromCanonical(canonicalEntries, scopeRoot, materializ
57
64
  description: exported.description,
58
65
  configFile: exported.configFile,
59
66
  rolePath: join(scopeRoot, '.codex', exported.configFile),
60
- content: exported.content,
67
+ content: withOatManagedCodexRoleOwner(exported.content, scopeOwner),
61
68
  sourcePath: entry.canonicalPath,
62
69
  });
63
70
  if (CODEX_MATERIALIZED_BASE_ROLES.has(exported.roleName)) {
@@ -329,9 +336,47 @@ function parseConfigAgentTable(content) {
329
336
  return {};
330
337
  }
331
338
  }
332
- async function collectStaleManagedRoles(scopeRoot, existingConfigContent, desiredRoleNames, cleanupOwner, removeLegacyProjectRoles) {
339
+ function codexScopeOwner(scopeRoot, options) {
340
+ return isUserCodexScope(scopeRoot, options)
341
+ ? 'user-config'
342
+ : 'project-config';
343
+ }
344
+ function codexCleanupOwners(scopeRoot, options) {
345
+ return isUserCodexScope(scopeRoot, options)
346
+ ? ['user-config']
347
+ : ['supported-catalogue', 'project-config'];
348
+ }
349
+ /**
350
+ * Whether a partial sync removes a canonical agent. A removed file can no
351
+ * longer be parsed for the role name it declared, so such a sync runs the full
352
+ * stale cleanup against the roles every present canonical agent produces.
353
+ */
354
+ function removesCanonicalAgent(canonicalEntries, allowedCanonicalPaths) {
355
+ const presentFiles = new Set(canonicalEntries
356
+ .filter((entry) => entry.type === 'agent' && entry.isFile)
357
+ .map((entry) => entry.name));
358
+ return allowedCanonicalPaths.some((path) => {
359
+ const match = /^\.agents\/agents\/([^/]+\.md)$/.exec(path);
360
+ return match?.[1] !== undefined && !presentFiles.has(match[1]);
361
+ });
362
+ }
363
+ async function collectStaleManagedRoles(scopeRoot, existingConfigContent, desiredRoleNames, cleanupOwners, removeLegacyProjectRoles) {
333
364
  const agents = parseConfigAgentTable(existingConfigContent);
334
- const stale = new Set();
365
+ const staleFiles = new Set();
366
+ const staleTables = new Set();
367
+ const isStaleManagedRole = (roleName, roleContent) => {
368
+ if (!roleContent || !isOatManagedCodexRoleFile(roleContent, roleName)) {
369
+ return false;
370
+ }
371
+ const owner = readOatManagedCodexRoleOwner(roleContent);
372
+ const legacyProjectRole = removeLegacyProjectRoles &&
373
+ owner === null &&
374
+ SUPPORTED_CODEX_BASE_ROLES.some((baseRole) => roleName.startsWith(`${baseRole}-`));
375
+ return ((owner !== null && cleanupOwners.includes(owner)) || legacyProjectRole);
376
+ };
377
+ // Stale cleanup deletes files, so a candidate whose real path (through a
378
+ // symlinked file or `.codex/agents` directory) leaves the scope is skipped.
379
+ const isWithinScope = (rolePath) => validateRealPathWithinScope(rolePath, scopeRoot).then(() => true, () => false);
335
380
  for (const [roleName, roleConfig] of Object.entries(agents)) {
336
381
  if (desiredRoleNames.has(roleName)) {
337
382
  continue;
@@ -341,18 +386,12 @@ async function collectStaleManagedRoles(scopeRoot, existingConfigContent, desire
341
386
  continue;
342
387
  }
343
388
  const rolePath = join(scopeRoot, '.codex', configFile);
344
- const roleContent = await readOptionalFile(rolePath);
345
- if (!roleContent || !isOatManagedCodexRoleFile(roleContent, roleName)) {
389
+ if (!(await isWithinScope(rolePath)) ||
390
+ !isStaleManagedRole(roleName, await readOptionalFile(rolePath))) {
346
391
  continue;
347
392
  }
348
- const owner = readOatManagedCodexRoleOwner(roleContent);
349
- const legacyProjectRole = removeLegacyProjectRoles &&
350
- owner === null &&
351
- SUPPORTED_CODEX_BASE_ROLES.some((baseRole) => roleName.startsWith(`${baseRole}-`));
352
- if (owner !== cleanupOwner && !legacyProjectRole) {
353
- continue;
354
- }
355
- stale.add(roleName);
393
+ staleTables.add(roleName);
394
+ staleFiles.add(roleName);
356
395
  }
357
396
  const agentsDir = join(scopeRoot, '.codex', 'agents');
358
397
  let roleFiles;
@@ -367,28 +406,27 @@ async function collectStaleManagedRoles(scopeRoot, existingConfigContent, desire
367
406
  continue;
368
407
  }
369
408
  const roleName = basename(roleFile, '.toml');
370
- if (desiredRoleNames.has(roleName) || stale.has(roleName)) {
409
+ if (desiredRoleNames.has(roleName) || staleFiles.has(roleName)) {
371
410
  continue;
372
411
  }
373
412
  const rolePath = join(agentsDir, roleFile);
374
- const roleContent = await readOptionalFile(rolePath);
375
- if (!roleContent || !isOatManagedCodexRoleFile(roleContent, roleName)) {
376
- continue;
377
- }
378
- const owner = readOatManagedCodexRoleOwner(roleContent);
379
- const legacyProjectRole = removeLegacyProjectRoles &&
380
- owner === null &&
381
- SUPPORTED_CODEX_BASE_ROLES.some((baseRole) => roleName.startsWith(`${baseRole}-`));
382
- if (owner !== cleanupOwner && !legacyProjectRole) {
413
+ if (!(await isWithinScope(rolePath)) ||
414
+ !isStaleManagedRole(roleName, await readOptionalFile(rolePath))) {
383
415
  continue;
384
416
  }
385
- stale.add(roleName);
417
+ // A table of the same name that points elsewhere was re-pointed by the
418
+ // user; only the leftover file is OAT's to remove.
419
+ staleFiles.add(roleName);
386
420
  }
387
- return [...stale].sort((left, right) => left.localeCompare(right));
421
+ const sorted = (names) => [...names].sort((left, right) => left.localeCompare(right));
422
+ return { roleFiles: sorted(staleFiles), configTables: sorted(staleTables) };
388
423
  }
389
424
  export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntries, allowedCanonicalPaths, options = {}) {
390
425
  const isPartialSync = allowedCanonicalPaths !== undefined && allowedCanonicalPaths.length > 0;
426
+ const cleansStaleRoles = !isPartialSync ||
427
+ removesCanonicalAgent(canonicalEntries, allowedCanonicalPaths);
391
428
  const materializationTargets = await readCodexMaterializationTargets(scopeRoot, options);
429
+ const scopeOwner = codexScopeOwner(scopeRoot, options);
392
430
  if (!isPartialSync &&
393
431
  isUserCodexScope(scopeRoot, options) &&
394
432
  materializationTargets.length > 0) {
@@ -400,11 +438,20 @@ export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntri
400
438
  throw new CliError(`Bundled managed Codex role definitions are unavailable for user sync: ${missingBaseRoles.join(', ')}. Refusing stale user-role cleanup.`);
401
439
  }
402
440
  }
403
- const desiredRoles = await desiredRolesFromCanonical(canonicalEntries.filter((entry) => canonicalPathAllowed(scopeRoot, entry, allowedCanonicalPaths)), scopeRoot, materializationTargets);
441
+ const desiredRoles = await desiredRolesFromCanonical(canonicalEntries.filter((entry) => canonicalPathAllowed(scopeRoot, entry, allowedCanonicalPaths)), scopeRoot, materializationTargets, scopeOwner);
404
442
  const desiredRoleNames = new Set(desiredRoles.map((role) => role.roleName));
405
443
  const existingConfigPath = configPath(scopeRoot);
406
444
  const existingConfigContent = await readOptionalFile(existingConfigPath);
407
- if (isPartialSync && desiredRoles.length === 0) {
445
+ const producedRoleNames = isPartialSync && cleansStaleRoles
446
+ ? new Set((await desiredRolesFromCanonical(canonicalEntries, scopeRoot, materializationTargets, scopeOwner)).map((role) => role.roleName))
447
+ : desiredRoleNames;
448
+ const staleRoles = cleansStaleRoles
449
+ ? await collectStaleManagedRoles(scopeRoot, existingConfigContent, producedRoleNames, codexCleanupOwners(scopeRoot, options), !isUserCodexScope(scopeRoot, options))
450
+ : { roleFiles: [], configTables: [] };
451
+ if (isPartialSync &&
452
+ desiredRoles.length === 0 &&
453
+ staleRoles.roleFiles.length === 0 &&
454
+ staleRoles.configTables.length === 0) {
408
455
  const aggregateConfigHash = hashContent(existingConfigContent ?? '');
409
456
  return {
410
457
  provider: 'codex',
@@ -420,9 +467,6 @@ export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntri
420
467
  };
421
468
  }
422
469
  const inheritedMaxDepth = await readInheritedCodexMaxDepth(scopeRoot, options);
423
- const staleRoles = isPartialSync
424
- ? []
425
- : await collectStaleManagedRoles(scopeRoot, existingConfigContent, desiredRoleNames, isUserCodexScope(scopeRoot, options) ? 'user-config' : 'project-config', !isUserCodexScope(scopeRoot, options));
426
470
  const operations = [];
427
471
  for (const role of desiredRoles) {
428
472
  const existingRoleContent = await readOptionalFile(role.rolePath);
@@ -462,10 +506,12 @@ export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntri
462
506
  roleName: role.roleName,
463
507
  });
464
508
  }
465
- for (const staleRole of staleRoles) {
509
+ for (const staleRole of staleRoles.roleFiles) {
466
510
  const staleRolePath = join(scopeRoot, '.codex', 'agents', `${staleRole}.toml`);
467
511
  const staleRoleContent = await readOptionalFile(staleRolePath);
468
- if (staleRoleContent &&
512
+ const staleRoleWithinScope = await validateRealPathWithinScope(staleRolePath, scopeRoot).then(() => true, () => false);
513
+ if (staleRoleWithinScope &&
514
+ staleRoleContent &&
469
515
  isOatManagedCodexRoleFile(staleRoleContent, staleRole)) {
470
516
  operations.push({
471
517
  provider: 'codex',
@@ -481,7 +527,7 @@ export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntri
481
527
  const configMerge = mergeCodexConfig({
482
528
  existingContent: existingConfigContent,
483
529
  desiredRoles: normalizeManagedRolesConfig(desiredRoles),
484
- staleManagedRoles: staleRoles,
530
+ staleManagedRoles: staleRoles.configTables,
485
531
  inheritedMaxDepth,
486
532
  });
487
533
  operations.push({
@@ -493,16 +539,16 @@ export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntri
493
539
  : 'skip',
494
540
  target: 'config',
495
541
  path: '.codex/config.toml',
496
- reason: existingConfigContent === null
542
+ reason: describeConfigOperation(existingConfigContent === null
497
543
  ? 'codex config missing'
498
544
  : configMerge.changed
499
545
  ? 'codex config differs from desired managed state'
500
- : 'codex config already in sync',
546
+ : 'codex config already in sync', configMerge),
501
547
  content: configMerge.mergedContent,
502
548
  });
503
549
  const managedRoles = [
504
550
  ...desiredRoles.map((role) => role.roleName),
505
- ...staleRoles,
551
+ ...staleRoles.roleFiles,
506
552
  ];
507
553
  const aggregateConfigHash = hashContent(configMerge.mergedContent);
508
554
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"materialize.d.ts","sourceRoot":"","sources":["../../../../src/providers/cursor/codec/materialize.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAKhE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAEvD,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,UAAU,CAAC;AAQlB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,sBAAsB,CAAC;IAC9B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,KAAK,EAAE,eAAe,CAAC;CACxB;AAWD,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,OAAO,EACP,KAAK,GACN,EAAE,6BAA6B,GAAG,uBAAuB,CA8BzD;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,MAAM,EAAE,sBAAsB,EAAE,CAAC;IACjC,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1C,KAAK,EAAE,eAAe,CAAC;CACxB,GAAG,uBAAuB,EAAE,CAwB5B;AAqCD,wBAAsB,sCAAsC,CAC1D,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CA0Ef"}
1
+ {"version":3,"file":"materialize.d.ts","sourceRoot":"","sources":["../../../../src/providers/cursor/codec/materialize.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAKhE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAEvD,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,UAAU,CAAC;AAQlB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,sBAAsB,CAAC;IAC9B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,KAAK,EAAE,eAAe,CAAC;CACxB;AAWD,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,OAAO,EACP,KAAK,GACN,EAAE,6BAA6B,GAAG,uBAAuB,CA8BzD;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,MAAM,EAAE,sBAAsB,EAAE,CAAC;IACjC,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1C,KAAK,EAAE,eAAe,CAAC;CACxB,GAAG,uBAAuB,EAAE,CAwB5B;AAgDD,wBAAsB,sCAAsC,CAC1D,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CA+Ef"}
@@ -1,4 +1,4 @@
1
- import { lstat, readFile, readdir } from 'node:fs/promises';
1
+ import { lstat, readFile, readdir, stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { CliError } from '../../../errors/index.js';
4
4
  import { validateRealPathWithinScope } from '../../../fs/paths.js';
@@ -70,6 +70,17 @@ async function readPathStats(path) {
70
70
  throw error;
71
71
  }
72
72
  }
73
+ async function isDanglingLink(path) {
74
+ try {
75
+ await stat(path);
76
+ return false;
77
+ }
78
+ catch (error) {
79
+ // Other failures, such as a link loop, fall through to the scope check
80
+ // and its controlled refusal.
81
+ return error.code === 'ENOENT';
82
+ }
83
+ }
73
84
  function readDeclaredCursorAgentName(content) {
74
85
  const normalized = content.startsWith('\uFEFF') ? content.slice(1) : content;
75
86
  const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(normalized);
@@ -118,6 +129,11 @@ export async function assertNoUnmanagedCursorAgentCollisions(scopeRoot, desiredR
118
129
  continue;
119
130
  }
120
131
  if (fileStats.isSymbolicLink()) {
132
+ // A dangling link cannot define a role, and the main sync plan
133
+ // removes the manifest-tracked view it was created as.
134
+ if (await isDanglingLink(absolutePath)) {
135
+ continue;
136
+ }
121
137
  try {
122
138
  await validateRealPathWithinScope(absolutePath, scopeRoot);
123
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.3.38",
3
+ "version": "0.3.39",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -41,8 +41,8 @@
41
41
  "unified": "11.0.5",
42
42
  "yaml": "2.8.2",
43
43
  "zod": "^3.25.76",
44
- "@open-agent-toolkit/control-plane": "0.3.38",
45
- "@open-agent-toolkit/docs-transforms": "0.3.38"
44
+ "@open-agent-toolkit/control-plane": "0.3.39",
45
+ "@open-agent-toolkit/docs-transforms": "0.3.39"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.10.0",