@open-agent-toolkit/cli 0.3.37 → 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 (84) 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 +8 -5
  11. package/assets/docs/reference/oat-directory-structure.md +1 -1
  12. package/assets/docs/reference/project-artifacts.md +12 -3
  13. package/assets/docs/reference/troubleshooting.md +4 -5
  14. package/assets/docs/reference/what-oat-writes.md +54 -59
  15. package/assets/docs/skills/repo-improve.md +23 -19
  16. package/assets/docs/workflows/advanced/workflow-gates.md +7 -1
  17. package/assets/docs/workflows/projects/closeout/closeout-skills.md +9 -6
  18. package/assets/docs/workflows/projects/execution/execution-skills.md +1 -2
  19. package/assets/docs/workflows/projects/execution/implementation-execution.md +3 -2
  20. package/assets/docs/workflows/projects/lifecycle.md +3 -1
  21. package/assets/public-package-versions.json +4 -4
  22. package/assets/skills/oat-project-brief/SKILL.md +1 -1
  23. package/assets/skills/oat-project-brief/references/brief-contract.md +32 -9
  24. package/assets/skills/oat-project-complete/SKILL.md +17 -7
  25. package/assets/skills/oat-project-implement/SKILL.md +2 -2
  26. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +14 -4
  27. package/assets/skills/oat-project-pr-final/SKILL.md +2 -2
  28. package/assets/skills/oat-project-pr-progress/SKILL.md +23 -12
  29. package/assets/skills/oat-repo-knowledge-index/SKILL.md +226 -131
  30. package/dist/commands/doctor/index.d.ts.map +1 -1
  31. package/dist/commands/doctor/index.js +5 -2
  32. package/dist/commands/init/index.d.ts.map +1 -1
  33. package/dist/commands/init/index.js +10 -5
  34. package/dist/commands/init/tools/index.d.ts.map +1 -1
  35. package/dist/commands/init/tools/index.js +106 -28
  36. package/dist/commands/init/tools/project-guidance.js +1 -1
  37. package/dist/commands/project/closeout-check/index.d.ts.map +1 -1
  38. package/dist/commands/project/closeout-check/index.js +4 -8
  39. package/dist/commands/project/delivery/check.d.ts.map +1 -1
  40. package/dist/commands/project/delivery/check.js +32 -14
  41. package/dist/commands/project/delivery/evidence.d.ts +39 -0
  42. package/dist/commands/project/delivery/evidence.d.ts.map +1 -1
  43. package/dist/commands/project/delivery/evidence.js +163 -17
  44. package/dist/commands/project/delivery/refresh.d.ts.map +1 -1
  45. package/dist/commands/project/delivery/refresh.js +13 -16
  46. package/dist/commands/project/journal-check/index.d.ts +2 -0
  47. package/dist/commands/project/journal-check/index.d.ts.map +1 -1
  48. package/dist/commands/project/journal-check/index.js +78 -1
  49. package/dist/commands/project/journal-check/types.d.ts +1 -1
  50. package/dist/commands/project/journal-check/types.d.ts.map +1 -1
  51. package/dist/commands/project/new/index.d.ts.map +1 -1
  52. package/dist/commands/project/new/index.js +1 -0
  53. package/dist/commands/project/new/scaffold.d.ts +1 -1
  54. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  55. package/dist/commands/project/new/scaffold.js +1 -1
  56. package/dist/commands/providers/codex/materialize.d.ts +5 -1
  57. package/dist/commands/providers/codex/materialize.d.ts.map +1 -1
  58. package/dist/commands/providers/codex/materialize.js +6 -1
  59. package/dist/commands/shared/exact-path-commit.d.ts.map +1 -1
  60. package/dist/commands/shared/exact-path-commit.js +11 -1
  61. package/dist/commands/shared/scope-option.d.ts +3 -1
  62. package/dist/commands/shared/scope-option.d.ts.map +1 -1
  63. package/dist/commands/shared/scope-option.js +4 -2
  64. package/dist/commands/status/index.d.ts.map +1 -1
  65. package/dist/commands/status/index.js +4 -1
  66. package/dist/commands/tools/install/index.d.ts.map +1 -1
  67. package/dist/commands/tools/install/index.js +1 -1
  68. package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts +11 -0
  69. package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts.map +1 -1
  70. package/dist/commands/tools/shared/pack-lifecycle-outcome.js +16 -0
  71. package/dist/config/oat-config.d.ts +6 -1
  72. package/dist/config/oat-config.d.ts.map +1 -1
  73. package/dist/config/oat-config.js +14 -22
  74. package/dist/index.d.ts +6 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +16 -0
  77. package/dist/providers/codex/codec/config-merge.d.ts +19 -0
  78. package/dist/providers/codex/codec/config-merge.d.ts.map +1 -1
  79. package/dist/providers/codex/codec/config-merge.js +75 -8
  80. package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
  81. package/dist/providers/codex/codec/sync-extension.js +84 -38
  82. package/dist/providers/cursor/codec/materialize.d.ts.map +1 -1
  83. package/dist/providers/cursor/codec/materialize.js +17 -1
  84. package/package.json +3 -3
@@ -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.37",
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.37",
45
- "@open-agent-toolkit/docs-transforms": "0.3.37"
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",