@open-agent-toolkit/cli 0.0.31 → 0.0.32

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.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Open Agent Toolkit command-line interface for provider sync, docs tooling, workflow utilities, and diagnostics.
4
4
 
5
+ The CLI also supports project-scoped instruction sync for nested `AGENTS.md` / `CLAUDE.md` files, including pointer, symlink, and hard-copy repair strategies plus Claude-only adoption.
6
+
5
7
  ## Install
6
8
 
7
9
  ```bash
@@ -26,6 +28,8 @@ oat config describe
26
28
  Additional useful entry points:
27
29
 
28
30
  - `oat tools install`
31
+ - `oat instructions validate --strategy pointer`
32
+ - `oat instructions sync --dry-run --strategy symlink`
29
33
  - `oat docs init --app-name my-docs`
30
34
  - `oat config dump --json`
31
35
  - `oat project status --json`
@@ -50,4 +54,5 @@ Use these commands when you want structured runtime/project state out of the CLI
50
54
  - [Docs Home](https://voxmedia.github.io/open-agent-toolkit/)
51
55
  - [CLI Utilities](https://voxmedia.github.io/open-agent-toolkit/cli-utilities)
52
56
  - [Provider Sync](https://voxmedia.github.io/open-agent-toolkit/provider-sync)
57
+ - [Instruction Sync](https://voxmedia.github.io/open-agent-toolkit/provider-sync/instruction-sync)
53
58
  - [Reference](https://voxmedia.github.io/open-agent-toolkit/reference)
@@ -80,12 +80,20 @@ Use these reference pages for file ownership and schema details:
80
80
 
81
81
  ## `oat instructions ...`
82
82
 
83
- These commands validate and repair pointer integrity between `AGENTS.md` and sibling `CLAUDE.md` files.
83
+ These commands validate and repair project-scoped instruction integrity between `AGENTS.md` and sibling `CLAUDE.md` files.
84
84
 
85
- - `oat instructions validate` - read-only integrity check
86
- - `oat instructions sync` - preview or apply pointer repairs
85
+ - `oat instructions validate` - read-only integrity check with `--strategy pointer|symlink|copy`
86
+ - `oat instructions sync` - preview or apply pointer, symlink, or hard-copy repairs
87
87
 
88
- Use this command group when instruction files drift after manual edits or generated updates.
88
+ Use this command group when instruction files drift after manual edits or generated updates, or when nested project directories contain Claude-only stray files that should be adopted into canonical `AGENTS.md`.
89
+
90
+ Operational notes:
91
+
92
+ - Validation and sync use the same recursive scan model, so `--dry-run` previews the same states that `validate` reports.
93
+ - `pointer` is the default strategy; `symlink` and `copy` make file shape part of correctness.
94
+ - Unreadable canonical `AGENTS.md` files and unreadable Claude-only sources are surfaced as drift, but sync leaves them in manual-repair mode instead of guessing at recovery.
95
+
96
+ For the full state model, repair semantics, and examples, see [Instruction Sync](../provider-sync/instruction-sync.md).
89
97
 
90
98
  ## Repo state helpers
91
99
 
@@ -84,23 +84,30 @@ These commands are documented here because they are commonly used during interop
84
84
 
85
85
  Purpose:
86
86
 
87
- - Validate AGENTS.md to CLAUDE.md pointer integrity
87
+ - Validate project-scoped `AGENTS.md` to `CLAUDE.md` integrity
88
88
 
89
89
  Key behavior:
90
90
 
91
- - Read-only validation of sibling `CLAUDE.md` pointer files for each discovered `AGENTS.md`
92
- - Reports `ok`, `missing`, and `content_mismatch` states
91
+ - Read-only validation of nested project-scoped instruction directories
92
+ - Supports `--strategy pointer|symlink|copy` to validate the expected file shape
93
+ - Reports `ok`, `missing`, `content_mismatch`, and `stray` states
94
+ - Detects Claude-only adoptable directories and unreadable/broken instruction paths as drift
93
95
  - Exit code `0` when all entries are valid, `1` when drift is detected
96
+ - Detailed behavior: [`Instruction Sync`](instruction-sync.md)
94
97
 
95
98
  ## `oat instructions sync`
96
99
 
97
100
  Purpose:
98
101
 
99
- - Repair AGENTS.md to CLAUDE.md pointer drift
102
+ - Repair project-scoped `AGENTS.md` to `CLAUDE.md` drift
100
103
 
101
104
  Key behavior:
102
105
 
103
106
  - Mutates by default; use `--dry-run` to preview changes
104
- - Creates missing `CLAUDE.md` pointers
107
+ - Supports `--strategy pointer|symlink|copy`
108
+ - Creates missing `CLAUDE.md` files using the selected strategy
109
+ - Adopts Claude-only stray files by writing canonical `AGENTS.md` content first, then regenerating `CLAUDE.md`
105
110
  - Skips mismatched files unless `--force` is provided
106
- - Writes canonical pointer content `@AGENTS.md\n`
111
+ - Skips unreadable canonical or Claude-only sources and reports manual-repair guidance instead of forcing recovery
112
+ - Uses pointer content `@AGENTS.md\n`, file symlinks, or hard copies depending on the selected strategy
113
+ - Detailed behavior and examples: [`Instruction Sync`](instruction-sync.md)
@@ -28,6 +28,7 @@ This section explains how OAT treats `.agents/` and `.oat/` as the source of tru
28
28
  ## Common Tasks
29
29
 
30
30
  - Understand the canonical/provider-view model in [Scope and Surface](scope-and-surface.md).
31
+ - Manage nested `AGENTS.md` / `CLAUDE.md` integrity in [Instruction Sync](instruction-sync.md).
31
32
  - Learn provider-specific mappings in [Providers](providers.md).
32
33
  - Diagnose drift and adoption behavior in [Manifest and Drift](manifest-and-drift.md).
33
34
  - Adjust provider enablement and scope behavior in [Sync Config](config.md).
@@ -38,6 +39,7 @@ This section explains how OAT treats `.agents/` and `.oat/` as the source of tru
38
39
  - [CLI Bootstrap](../cli-utilities/bootstrap.md) - Foundational setup before first sync.
39
40
  - [Scope and Surface](scope-and-surface.md) - Canonical assets, provider views, scopes, and the sync surface area.
40
41
  - [Commands](commands.md) - `oat status`, `oat sync`, and `oat providers ...` behavior.
42
+ - [Instruction Sync](instruction-sync.md) - Project-scoped `AGENTS.md` / `CLAUDE.md` validation, repair, and Claude-only adoption.
41
43
  - [Providers](providers.md) - Provider-specific mappings, capabilities, and path conventions.
42
44
  - [Manifest and Drift](manifest-and-drift.md) - How OAT tracks synced state, stray files, and adoption decisions.
43
45
  - [Sync Config](config.md) - Provider config model, enablement, and scope semantics.
@@ -0,0 +1,146 @@
1
+ ---
2
+ title: Instruction Sync
3
+ description: Project-scoped AGENTS.md and CLAUDE.md validation, repair strategies, and Claude-only adoption.
4
+ ---
5
+
6
+ # Instruction Sync
7
+
8
+ `oat instructions ...` is the project-scoped lane for keeping canonical `AGENTS.md` files aligned with sibling `CLAUDE.md` files throughout a repository tree.
9
+
10
+ Use it when you want OAT to:
11
+
12
+ - validate nested `AGENTS.md` / `CLAUDE.md` pairs
13
+ - repair missing or drifted `CLAUDE.md` files with a chosen strategy
14
+ - adopt Claude-only directories back into canonical `AGENTS.md`
15
+
16
+ This command group is intentionally separate from manifest-backed provider sync. It operates on repo-local instruction files, not provider view manifests.
17
+
18
+ ## Scope
19
+
20
+ Instruction sync is currently project-only.
21
+
22
+ - It scans the current repository recursively.
23
+ - It supports nested directories all the way down the tree.
24
+ - It skips provider-irrelevant or local-only roots such as `.git`, `.oat`, `.worktrees`, and `node_modules`.
25
+ - It does not scan user-level provider roots such as `~/.claude` in this release.
26
+
27
+ ## Canonical Model
28
+
29
+ - `AGENTS.md` is canonical.
30
+ - `CLAUDE.md` is derived from the sibling `AGENTS.md`.
31
+ - If a directory contains only `CLAUDE.md`, OAT can adopt that file by writing canonical `AGENTS.md` content first and then regenerating `CLAUDE.md`.
32
+
33
+ ## Commands
34
+
35
+ ### `oat instructions validate`
36
+
37
+ Read-only validation for instruction integrity.
38
+
39
+ - Resolves the project root automatically.
40
+ - Scans nested directories for instruction pairs and Claude-only strays.
41
+ - Returns exit code `0` when everything is valid and `1` when drift is detected.
42
+ - Suggests the matching repair command, including `--strategy` when you validated with a non-default mode.
43
+
44
+ ### `oat instructions sync`
45
+
46
+ Repair and adoption command for instruction drift.
47
+
48
+ - Mutates by default.
49
+ - Use `--dry-run` to preview planned actions.
50
+ - Use `--force` to overwrite mismatched `CLAUDE.md` files.
51
+ - Reuses the selected strategy when creating or repairing `CLAUDE.md`.
52
+
53
+ ## Supported Strategies
54
+
55
+ `CLAUDE.md` can be generated or validated in one of three modes:
56
+
57
+ | Strategy | Expected `CLAUDE.md` shape | Notes |
58
+ | --------- | -------------------------------- | --------------------------------------- |
59
+ | `pointer` | file content `@AGENTS.md` | Default mode. Lightweight and explicit. |
60
+ | `symlink` | file symlink to `AGENTS.md` | Uses a same-directory relative symlink. |
61
+ | `copy` | hard copy of `AGENTS.md` content | Useful when symlinks are undesirable. |
62
+
63
+ Validation treats the selected file shape as part of correctness. For example, `copy` mode rejects a symlink even if the symlink resolves to identical content.
64
+
65
+ ## Reported States
66
+
67
+ `oat instructions validate` and `oat instructions sync --dry-run` work from the same scan model.
68
+
69
+ | State | Meaning |
70
+ | ------------------ | ------------------------------------------------------------------------------------------------ |
71
+ | `ok` | The discovered instruction pair matches the selected strategy. |
72
+ | `missing` | `AGENTS.md` exists but sibling `CLAUDE.md` is missing. |
73
+ | `content_mismatch` | `CLAUDE.md` exists but has the wrong shape/content, or an instruction file is unreadable/broken. |
74
+ | `stray` | `CLAUDE.md` exists without sibling `AGENTS.md` and the Claude file is readable enough to adopt. |
75
+
76
+ ## Claude-Only Adoption
77
+
78
+ When a directory contains `CLAUDE.md` but no `AGENTS.md`, sync can adopt it:
79
+
80
+ 1. Read the existing `CLAUDE.md`
81
+ 2. Write canonical `AGENTS.md` with that content
82
+ 3. Regenerate `CLAUDE.md` using the selected strategy
83
+
84
+ This means the original Claude instructions become canonical before the derived file is rewritten.
85
+
86
+ Readable Claude-only files are adoptable. Unreadable Claude-only files are not.
87
+
88
+ ## When `--force` Is Required
89
+
90
+ `oat instructions sync` does not overwrite a mismatched `CLAUDE.md` unless you pass `--force`.
91
+
92
+ Typical pattern:
93
+
94
+ ```bash
95
+ oat instructions validate --strategy symlink
96
+ oat instructions sync --dry-run --strategy symlink
97
+ oat instructions sync --force --strategy symlink
98
+ oat instructions validate --strategy symlink
99
+ ```
100
+
101
+ This keeps validation, preview, and repair aligned to the same expected file shape.
102
+
103
+ ## Manual-Repair Cases
104
+
105
+ Some states are intentionally surfaced as drift but not auto-repaired.
106
+
107
+ Examples:
108
+
109
+ - unreadable canonical `AGENTS.md`
110
+ - broken or unreadable `AGENTS.md` symlink targets
111
+ - unreadable Claude-only sources
112
+ - broken or unreadable paired `CLAUDE.md` symlink targets
113
+
114
+ In these cases, sync reports a manual-repair skip instead of guessing at destructive recovery.
115
+
116
+ ## Example Workflow
117
+
118
+ Preview and adopt nested Claude-only strays as pointer files:
119
+
120
+ ```bash
121
+ oat instructions sync --dry-run
122
+ oat instructions sync
123
+ ```
124
+
125
+ Validate and repair everything as symlinks:
126
+
127
+ ```bash
128
+ oat instructions validate --strategy symlink
129
+ oat instructions sync --dry-run --strategy symlink
130
+ oat instructions sync --force --strategy symlink
131
+ ```
132
+
133
+ Validate and repair everything as hard copies:
134
+
135
+ ```bash
136
+ oat instructions validate --strategy copy
137
+ oat instructions sync --dry-run --strategy copy
138
+ oat instructions sync --force --strategy copy
139
+ ```
140
+
141
+ ## Related Pages
142
+
143
+ - [Provider Interop Commands](commands.md)
144
+ - [Provider Interop CLI Scope and Surface](scope-and-surface.md)
145
+ - [Config and Local State](../cli-utilities/config-and-local-state.md)
146
+ - [Troubleshooting](../reference/troubleshooting.md)
@@ -7,7 +7,7 @@ description: 'Scope boundaries and design principles for canonical-to-provider a
7
7
 
8
8
  The provider interop CLI in `packages/cli` manages canonical agent assets under `.agents/` and reconciles provider-specific views.
9
9
 
10
- This capability is intentionally independent from OAT workflow artifacts. Teams can adopt provider interoperability usage (`status`, `sync`, `providers ...`) plus optional instruction-pointer integrity checks (`instructions validate/sync`) without using discovery/spec/design/plan/implement project workflows.
10
+ This capability is intentionally independent from OAT workflow artifacts. Teams can adopt provider interoperability usage (`status`, `sync`, `providers ...`) plus optional project-scoped instruction sync integrity checks (`instructions validate/sync`) without using discovery/spec/design/plan/implement project workflows.
11
11
 
12
12
  ## Scope
13
13
 
@@ -53,7 +53,7 @@ Rules are currently project-scoped canonical content. Unlike skills and agents,
53
53
  ## Non-interop namespaces in the same CLI
54
54
 
55
55
  - `oat project new <name>` (workflow/project scaffolding)
56
- - `oat instructions validate` / `oat instructions sync` (AGENTS.md to CLAUDE.md pointer integrity)
56
+ - `oat instructions validate` / `oat instructions sync` (AGENTS.md/CLAUDE.md pointer, symlink, or copy integrity plus Claude-only adoption)
57
57
  - `oat internal validate-oat-skills` (internal maintenance)
58
58
 
59
59
  ## Reference artifacts
@@ -27,13 +27,18 @@ Expected for native-read skill mappings. Codex can read canonical skills without
27
27
  - `oat providers set --scope project --enabled <providers> --disabled <providers>`
28
28
  - Re-run `oat sync --scope project` after updating config.
29
29
 
30
- ## `instructions validate` reports `missing` or `content_mismatch`
31
-
32
- - Run `oat instructions sync` to preview changes.
33
- - Run `oat instructions sync` to create missing pointer files.
34
- - If mismatched `CLAUDE.md` files should be overwritten, run `oat instructions sync --force`.
30
+ ## `instructions validate` reports `missing`, `content_mismatch`, or `stray`
31
+
32
+ - Run `oat instructions sync --dry-run` to preview changes.
33
+ - Run `oat instructions sync --strategy pointer|symlink|copy` to apply the expected `CLAUDE.md` shape.
34
+ - If mismatched `CLAUDE.md` files should be overwritten, run `oat instructions sync --force` (or combine it with `--strategy` if needed).
35
+ - If `stray` is reported, `oat instructions sync` will adopt the Claude-only file into `AGENTS.md` and then regenerate `CLAUDE.md`.
36
+ - If a broken or unreadable instruction path is reported, fix the underlying file or symlink target first; sync will intentionally skip manual-repair cases instead of forcing recovery.
37
+ - If a directory you expected to see is missing from the scan, confirm it is not under `.git`, `.oat`, `.worktrees`, or `node_modules`.
35
38
  - Re-run `oat instructions validate` and confirm status is `ok`.
36
39
 
40
+ Use [Instruction Sync](../provider-sync/instruction-sync.md) for the full strategy matrix and state model.
41
+
37
42
  ## `doctor` warns about canonical directories
38
43
 
39
44
  - Run `oat init` for the relevant scope.
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli": "0.0.31",
3
- "docs-config": "0.0.31",
4
- "docs-theme": "0.0.31",
5
- "docs-transforms": "0.0.31"
2
+ "cli": "0.0.32",
3
+ "docs-config": "0.0.32",
4
+ "docs-theme": "0.0.32",
5
+ "docs-transforms": "0.0.32"
6
6
  }
@@ -1,9 +1,11 @@
1
1
  import type { Dirent, Stats } from 'node:fs';
2
2
  import type { CommandContext, GlobalOptions } from '../../app/command-context.js';
3
- export type InstructionStatus = 'ok' | 'missing' | 'content_mismatch';
3
+ export declare const INSTRUCTION_SYNC_STRATEGIES: readonly ["pointer", "symlink", "copy"];
4
+ export type InstructionSyncStrategy = (typeof INSTRUCTION_SYNC_STRATEGIES)[number];
5
+ export type InstructionStatus = 'ok' | 'missing' | 'content_mismatch' | 'stray';
4
6
  export type InstructionsStatus = 'ok' | 'drift';
5
7
  export interface InstructionEntry {
6
- agentsPath: string;
8
+ agentsPath: string | null;
7
9
  claudePath: string;
8
10
  status: InstructionStatus;
9
11
  detail: string;
@@ -22,6 +24,7 @@ export interface InstructionsSummary {
22
24
  ok: number;
23
25
  missing: number;
24
26
  contentMismatch: number;
27
+ stray: number;
25
28
  created: number;
26
29
  updated: number;
27
30
  skipped: number;
@@ -33,20 +36,30 @@ export interface InstructionsJsonPayload {
33
36
  entries: InstructionEntry[];
34
37
  actions: InstructionActionRecord[];
35
38
  }
39
+ export interface InstructionsScanOptions {
40
+ strategy?: InstructionSyncStrategy;
41
+ debug?: (message: string) => void;
42
+ }
36
43
  export interface InstructionsScanDependencies {
37
44
  readdir: (path: string, options: {
38
45
  withFileTypes: true;
39
46
  }) => Promise<Dirent[]>;
47
+ lstat: (path: string) => Promise<Stats>;
48
+ realpath: (path: string) => Promise<string>;
40
49
  readFile: (path: string, encoding: 'utf8') => Promise<string>;
50
+ readlink: (path: string) => Promise<string>;
41
51
  stat: (path: string) => Promise<Stats>;
42
- debug?: (message: string) => void;
43
52
  }
44
53
  export interface InstructionsValidateCommandDependencies {
45
54
  buildCommandContext: (options: GlobalOptions) => CommandContext;
46
55
  resolveProjectRoot: (cwd: string) => Promise<string>;
47
- scanInstructionFiles: (repoRoot: string, overrides?: Partial<InstructionsScanDependencies>) => Promise<InstructionEntry[]>;
56
+ scanInstructionFiles: (repoRoot: string, options?: InstructionsScanOptions, overrides?: Partial<InstructionsScanDependencies>) => Promise<InstructionEntry[]>;
48
57
  }
49
58
  export interface InstructionsSyncCommandDependencies extends InstructionsValidateCommandDependencies {
59
+ lstat: (path: string) => Promise<Stats>;
60
+ readFile: (path: string, encoding: 'utf8') => Promise<string>;
61
+ removeFile: (path: string) => Promise<void>;
62
+ symlinkFile: (target: string, path: string) => Promise<void>;
50
63
  writeFile: (path: string, content: string, encoding: 'utf8') => Promise<void>;
51
64
  }
52
65
  //# sourceMappingURL=instructions.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instructions.types.d.ts","sourceRoot":"","sources":["../../../src/commands/instructions/instructions.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1E,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAEtE,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEjE,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,uBAAuB,CAAC;CACjC;AAED,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,mBAAmB,CAAC;IAC7B,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,CACP,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;QAAE,aAAa,EAAE,IAAI,CAAA;KAAE,KAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IACvC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,uCAAuC;IACtD,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,oBAAoB,EAAE,CACpB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,OAAO,CAAC,4BAA4B,CAAC,KAC9C,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,mCAAoC,SAAQ,uCAAuC;IAClG,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E"}
1
+ {"version":3,"file":"instructions.types.d.ts","sourceRoot":"","sources":["../../../src/commands/instructions/instructions.types.ts"],"names":[],"mappings":"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"}
@@ -1 +1,5 @@
1
- export {};
1
+ export const INSTRUCTION_SYNC_STRATEGIES = [
2
+ 'pointer',
3
+ 'symlink',
4
+ 'copy',
5
+ ];
@@ -1,11 +1,13 @@
1
- import type { InstructionActionRecord, InstructionEntry, InstructionsJsonPayload, InstructionsMode, InstructionsScanDependencies, InstructionsSummary } from './instructions.types.js';
1
+ import type { InstructionSyncStrategy, InstructionActionRecord, InstructionEntry, InstructionsJsonPayload, InstructionsMode, InstructionsScanOptions, InstructionsScanDependencies, InstructionsSummary } from './instructions.types.js';
2
2
  export declare const EXPECTED_CLAUDE_CONTENT = "@AGENTS.md\n";
3
+ export declare const DEFAULT_INSTRUCTION_SYNC_STRATEGY: InstructionSyncStrategy;
3
4
  interface BuildInstructionsPayloadArgs {
4
5
  mode: InstructionsMode;
5
6
  entries: InstructionEntry[];
6
7
  actions: InstructionActionRecord[];
7
8
  }
8
- export declare function scanInstructionFiles(repoRoot: string, overrides?: Partial<InstructionsScanDependencies>): Promise<InstructionEntry[]>;
9
+ export declare function resolveInstructionSyncStrategy(strategy?: InstructionSyncStrategy): InstructionSyncStrategy;
10
+ export declare function scanInstructionFiles(repoRoot: string, options?: InstructionsScanOptions, overrides?: Partial<InstructionsScanDependencies>): Promise<InstructionEntry[]>;
9
11
  export declare function buildInstructionsSummary(entries: InstructionEntry[], actions: InstructionActionRecord[]): InstructionsSummary;
10
12
  export declare function buildInstructionsPayload({ mode, entries, actions, }: BuildInstructionsPayloadArgs): InstructionsJsonPayload;
11
13
  export declare function formatInstructionsReport(payload: InstructionsJsonPayload, repoRoot?: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"instructions.utils.d.ts","sourceRoot":"","sources":["../../../src/commands/instructions/instructions.utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,4BAA4B,EAE5B,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAKtD,UAAU,4BAA4B;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAoHD,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,OAAO,CAAC,4BAA4B,CAAM,GACpD,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAoD7B;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,OAAO,EAAE,uBAAuB,EAAE,GACjC,mBAAmB,CAmBrB;AAgBD,wBAAgB,wBAAwB,CAAC,EACvC,IAAI,EACJ,OAAO,EACP,OAAO,GACR,EAAE,4BAA4B,GAAG,uBAAuB,CAWxD;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,CAkCR"}
1
+ {"version":3,"file":"instructions.utils.d.ts","sourceRoot":"","sources":["../../../src/commands/instructions/instructions.utils.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAE5B,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AACtD,eAAO,MAAM,iCAAiC,EAAE,uBACrC,CAAC;AAKZ,UAAU,4BAA4B;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAqBD,wBAAgB,8BAA8B,CAC5C,QAAQ,CAAC,EAAE,uBAAuB,GACjC,uBAAuB,CAEzB;AAoLD,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,uBAA4B,EACrC,SAAS,GAAE,OAAO,CAAC,4BAA4B,CAAM,GACpD,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAiO7B;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,OAAO,EAAE,uBAAuB,EAAE,GACjC,mBAAmB,CAoBrB;AAgBD,wBAAgB,wBAAwB,CAAC,EACvC,IAAI,EACJ,OAAO,EACP,OAAO,GACR,EAAE,4BAA4B,GAAG,uBAAuB,CAWxD;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,CAmCR"}
@@ -1,6 +1,7 @@
1
- import { readdir, readFile, stat } from 'node:fs/promises';
2
- import { dirname, join, relative } from 'node:path';
1
+ import { lstat, readdir, readFile, readlink, realpath, stat, } from 'node:fs/promises';
2
+ import { dirname, join, relative, resolve } from 'node:path';
3
3
  export const EXPECTED_CLAUDE_CONTENT = '@AGENTS.md\n';
4
+ export const DEFAULT_INSTRUCTION_SYNC_STRATEGY = 'pointer';
4
5
  const ROOT_EXCLUDED_DIRECTORIES = new Set(['.git', '.oat', '.worktrees']);
5
6
  const GLOBAL_EXCLUDED_DIRECTORIES = new Set(['node_modules']);
6
7
  function getErrorCode(error) {
@@ -11,12 +12,37 @@ function getErrorCode(error) {
11
12
  function normalizeLineEndings(content) {
12
13
  return content.replaceAll('\r\n', '\n');
13
14
  }
15
+ export function resolveInstructionSyncStrategy(strategy) {
16
+ return strategy ?? DEFAULT_INSTRUCTION_SYNC_STRATEGY;
17
+ }
18
+ function getValidInstructionDetail(strategy) {
19
+ switch (strategy) {
20
+ case 'symlink':
21
+ return 'symlink valid';
22
+ case 'copy':
23
+ return 'copy valid';
24
+ default:
25
+ return 'pointer valid';
26
+ }
27
+ }
28
+ function getInvalidInstructionDetail(strategy) {
29
+ switch (strategy) {
30
+ case 'symlink':
31
+ return 'expected symlink to AGENTS.md';
32
+ case 'copy':
33
+ return 'expected hard copy of AGENTS.md content';
34
+ default:
35
+ return `expected ${JSON.stringify(EXPECTED_CLAUDE_CONTENT)}`;
36
+ }
37
+ }
14
38
  function toPosixPath(pathValue) {
15
39
  return pathValue.replaceAll('\\', '/');
16
40
  }
17
41
  function normalizeEntries(entries) {
18
42
  return [...entries].sort((left, right) => {
19
- return (left.agentsPath.localeCompare(right.agentsPath) ||
43
+ const leftSortPath = left.agentsPath ?? left.claudePath;
44
+ const rightSortPath = right.agentsPath ?? right.claudePath;
45
+ return (leftSortPath.localeCompare(rightSortPath) ||
20
46
  left.claudePath.localeCompare(right.claudePath) ||
21
47
  left.status.localeCompare(right.status) ||
22
48
  left.detail.localeCompare(right.detail));
@@ -30,9 +56,23 @@ function normalizeActions(actions) {
30
56
  left.reason.localeCompare(right.reason));
31
57
  });
32
58
  }
33
- async function scanAgentsFiles(repoRoot, dependencies) {
59
+ function recordInstructionFile(directoryEntries, directoryPath, entryName, entryPath) {
60
+ const current = directoryEntries.get(directoryPath) ?? {};
61
+ if (entryName === 'AGENTS.md') {
62
+ current.agentsPath = entryPath;
63
+ current.brokenAgentsPath = undefined;
64
+ current.brokenAgentsErrorCode = undefined;
65
+ }
66
+ else {
67
+ current.claudePath = entryPath;
68
+ current.brokenClaudePath = undefined;
69
+ current.brokenClaudeErrorCode = undefined;
70
+ }
71
+ directoryEntries.set(directoryPath, current);
72
+ }
73
+ async function scanInstructionDirectories(repoRoot, dependencies, debug) {
34
74
  const queue = [repoRoot];
35
- const agentsFiles = [];
75
+ const directoryEntries = new Map();
36
76
  while (queue.length > 0) {
37
77
  const currentDirectory = queue.shift();
38
78
  if (!currentDirectory) {
@@ -46,7 +86,7 @@ async function scanAgentsFiles(repoRoot, dependencies) {
46
86
  }
47
87
  catch (error) {
48
88
  const errorCode = getErrorCode(error);
49
- dependencies.debug?.(`Skipping directory scan for ${toPosixPath(currentDirectory)} (${errorCode ?? 'unknown error'})`);
89
+ debug?.(`Skipping directory scan for ${toPosixPath(currentDirectory)} (${errorCode ?? 'unknown error'})`);
50
90
  continue;
51
91
  }
52
92
  for (const entry of entries) {
@@ -63,8 +103,8 @@ async function scanAgentsFiles(repoRoot, dependencies) {
63
103
  continue;
64
104
  }
65
105
  if (entry.isFile()) {
66
- if (entry.name === 'AGENTS.md') {
67
- agentsFiles.push(entryPath);
106
+ if (entry.name === 'AGENTS.md' || entry.name === 'CLAUDE.md') {
107
+ recordInstructionFile(directoryEntries, currentDirectory, entry.name, entryPath);
68
108
  }
69
109
  continue;
70
110
  }
@@ -77,38 +117,157 @@ async function scanAgentsFiles(repoRoot, dependencies) {
77
117
  }
78
118
  catch (error) {
79
119
  const errorCode = getErrorCode(error);
80
- dependencies.debug?.(`Skipping symlink target stat for ${toPosixPath(entryPath)} (${errorCode ?? 'unknown error'})`);
120
+ if (entry.name === 'CLAUDE.md') {
121
+ const current = directoryEntries.get(currentDirectory) ?? {};
122
+ current.claudePath = entryPath;
123
+ current.brokenClaudePath = entryPath;
124
+ current.brokenClaudeErrorCode = errorCode ?? 'unknown error';
125
+ directoryEntries.set(currentDirectory, current);
126
+ continue;
127
+ }
128
+ if (entry.name === 'AGENTS.md') {
129
+ const current = directoryEntries.get(currentDirectory) ?? {};
130
+ current.brokenAgentsPath = entryPath;
131
+ current.brokenAgentsErrorCode = errorCode ?? 'unknown error';
132
+ directoryEntries.set(currentDirectory, current);
133
+ continue;
134
+ }
135
+ debug?.(`Skipping symlink target stat for ${toPosixPath(entryPath)} (${errorCode ?? 'unknown error'})`);
81
136
  continue;
82
137
  }
83
138
  if (entryStats.isDirectory()) {
84
139
  continue;
85
140
  }
86
- if (entryStats.isFile() && entry.name === 'AGENTS.md') {
87
- agentsFiles.push(entryPath);
141
+ if (entryStats.isFile() &&
142
+ (entry.name === 'AGENTS.md' || entry.name === 'CLAUDE.md')) {
143
+ recordInstructionFile(directoryEntries, currentDirectory, entry.name, entryPath);
88
144
  }
89
145
  }
90
146
  }
91
- return agentsFiles.sort((left, right) => left.localeCompare(right));
147
+ return directoryEntries;
92
148
  }
93
- export async function scanInstructionFiles(repoRoot, overrides = {}) {
149
+ export async function scanInstructionFiles(repoRoot, options = {}, overrides = {}) {
150
+ const strategy = resolveInstructionSyncStrategy(options.strategy);
94
151
  const dependencies = {
152
+ lstat,
153
+ realpath,
95
154
  readdir,
96
155
  readFile,
156
+ readlink,
97
157
  stat,
98
158
  ...overrides,
99
159
  };
100
- const agentsFiles = await scanAgentsFiles(repoRoot, dependencies);
160
+ const instructionDirectories = await scanInstructionDirectories(repoRoot, dependencies, options.debug);
101
161
  const entries = [];
102
- for (const agentsPath of agentsFiles) {
103
- const claudePath = join(dirname(agentsPath), 'CLAUDE.md');
162
+ for (const [directoryPath, directoryEntry] of instructionDirectories) {
163
+ const agentsPath = directoryEntry.agentsPath ?? null;
164
+ const brokenAgentsPath = directoryEntry.brokenAgentsPath ?? null;
165
+ const brokenAgentsErrorCode = directoryEntry.brokenAgentsErrorCode ?? 'unknown error';
166
+ const brokenClaudePath = directoryEntry.brokenClaudePath ?? null;
167
+ const brokenClaudeErrorCode = directoryEntry.brokenClaudeErrorCode ?? 'unknown error';
168
+ const claudePath = directoryEntry.claudePath ?? join(directoryPath, 'CLAUDE.md');
169
+ if (!agentsPath && brokenAgentsPath) {
170
+ entries.push({
171
+ agentsPath: brokenAgentsPath,
172
+ claudePath,
173
+ status: 'content_mismatch',
174
+ detail: brokenAgentsErrorCode === 'ENOENT'
175
+ ? 'broken AGENTS.md symlink'
176
+ : `unreadable AGENTS.md symlink target (${brokenAgentsErrorCode})`,
177
+ });
178
+ continue;
179
+ }
180
+ if (brokenClaudePath) {
181
+ entries.push({
182
+ agentsPath,
183
+ claudePath,
184
+ status: 'content_mismatch',
185
+ detail: brokenClaudeErrorCode === 'ENOENT'
186
+ ? 'broken CLAUDE.md symlink'
187
+ : `unreadable CLAUDE.md symlink target (${brokenClaudeErrorCode})`,
188
+ });
189
+ continue;
190
+ }
191
+ if (!agentsPath) {
192
+ try {
193
+ await dependencies.readFile(claudePath, 'utf8');
194
+ }
195
+ catch (error) {
196
+ entries.push({
197
+ agentsPath: null,
198
+ claudePath,
199
+ status: 'content_mismatch',
200
+ detail: `unable to read CLAUDE.md (${getErrorCode(error) ?? 'unknown'})`,
201
+ });
202
+ continue;
203
+ }
204
+ entries.push({
205
+ agentsPath: null,
206
+ claudePath,
207
+ status: 'stray',
208
+ detail: 'CLAUDE.md found without AGENTS.md',
209
+ });
210
+ continue;
211
+ }
212
+ let claudeStats;
104
213
  try {
105
- const claudeContent = await dependencies.readFile(claudePath, 'utf8');
106
- if (normalizeLineEndings(claudeContent) === EXPECTED_CLAUDE_CONTENT) {
214
+ claudeStats = await dependencies.lstat(claudePath);
215
+ }
216
+ catch (error) {
217
+ const errorCode = getErrorCode(error);
218
+ if (errorCode === 'ENOENT') {
219
+ entries.push({
220
+ agentsPath,
221
+ claudePath,
222
+ status: 'missing',
223
+ detail: 'CLAUDE.md missing',
224
+ });
225
+ }
226
+ else {
227
+ entries.push({
228
+ agentsPath,
229
+ claudePath,
230
+ status: 'content_mismatch',
231
+ detail: `unable to read CLAUDE.md (${errorCode ?? 'unknown error'})`,
232
+ });
233
+ }
234
+ continue;
235
+ }
236
+ if (strategy === 'symlink') {
237
+ if (!claudeStats.isSymbolicLink()) {
238
+ entries.push({
239
+ agentsPath,
240
+ claudePath,
241
+ status: 'content_mismatch',
242
+ detail: getInvalidInstructionDetail(strategy),
243
+ });
244
+ continue;
245
+ }
246
+ let claudeTarget;
247
+ try {
248
+ claudeTarget = await dependencies.readlink(claudePath);
249
+ }
250
+ catch (error) {
251
+ const errorCode = getErrorCode(error);
252
+ entries.push({
253
+ agentsPath,
254
+ claudePath,
255
+ status: 'content_mismatch',
256
+ detail: `unable to read CLAUDE.md symlink target (${errorCode ?? 'unknown error'})`,
257
+ });
258
+ continue;
259
+ }
260
+ const resolvedTarget = resolve(dirname(claudePath), claudeTarget);
261
+ const [canonicalTarget, canonicalAgentsPath] = await Promise.all([
262
+ dependencies.realpath(resolvedTarget).catch(() => resolvedTarget),
263
+ dependencies.realpath(agentsPath).catch(() => agentsPath),
264
+ ]);
265
+ if (canonicalTarget === canonicalAgentsPath) {
107
266
  entries.push({
108
267
  agentsPath,
109
268
  claudePath,
110
269
  status: 'ok',
111
- detail: 'pointer valid',
270
+ detail: getValidInstructionDetail(strategy),
112
271
  });
113
272
  }
114
273
  else {
@@ -116,9 +275,23 @@ export async function scanInstructionFiles(repoRoot, overrides = {}) {
116
275
  agentsPath,
117
276
  claudePath,
118
277
  status: 'content_mismatch',
119
- detail: `expected ${JSON.stringify(EXPECTED_CLAUDE_CONTENT)}`,
278
+ detail: getInvalidInstructionDetail(strategy),
120
279
  });
121
280
  }
281
+ continue;
282
+ }
283
+ if (claudeStats.isSymbolicLink()) {
284
+ entries.push({
285
+ agentsPath,
286
+ claudePath,
287
+ status: 'content_mismatch',
288
+ detail: getInvalidInstructionDetail(strategy),
289
+ });
290
+ continue;
291
+ }
292
+ let claudeContent;
293
+ try {
294
+ claudeContent = await dependencies.readFile(claudePath, 'utf8');
122
295
  }
123
296
  catch (error) {
124
297
  const errorCode = getErrorCode(error);
@@ -138,6 +311,44 @@ export async function scanInstructionFiles(repoRoot, overrides = {}) {
138
311
  detail: `unable to read CLAUDE.md (${errorCode ?? 'unknown error'})`,
139
312
  });
140
313
  }
314
+ continue;
315
+ }
316
+ const expectedContent = strategy === 'copy'
317
+ ? await (async () => {
318
+ try {
319
+ return await dependencies.readFile(agentsPath, 'utf8');
320
+ }
321
+ catch (error) {
322
+ const errorCode = getErrorCode(error);
323
+ entries.push({
324
+ agentsPath,
325
+ claudePath,
326
+ status: 'content_mismatch',
327
+ detail: `unable to read AGENTS.md (${errorCode ?? 'unknown error'})`,
328
+ });
329
+ return null;
330
+ }
331
+ })()
332
+ : EXPECTED_CLAUDE_CONTENT;
333
+ if (expectedContent === null) {
334
+ continue;
335
+ }
336
+ if (normalizeLineEndings(claudeContent) ===
337
+ normalizeLineEndings(expectedContent)) {
338
+ entries.push({
339
+ agentsPath,
340
+ claudePath,
341
+ status: 'ok',
342
+ detail: getValidInstructionDetail(strategy),
343
+ });
344
+ }
345
+ else {
346
+ entries.push({
347
+ agentsPath,
348
+ claudePath,
349
+ status: 'content_mismatch',
350
+ detail: getInvalidInstructionDetail(strategy),
351
+ });
141
352
  }
142
353
  }
143
354
  return normalizeEntries(entries);
@@ -151,6 +362,7 @@ export function buildInstructionsSummary(entries, actions) {
151
362
  missing: normalizedEntries.filter((entry) => entry.status === 'missing')
152
363
  .length,
153
364
  contentMismatch: normalizedEntries.filter((entry) => entry.status === 'content_mismatch').length,
365
+ stray: normalizedEntries.filter((entry) => entry.status === 'stray').length,
154
366
  created: normalizedActions.filter((action) => action.type === 'create')
155
367
  .length,
156
368
  updated: normalizedActions.filter((action) => action.type === 'update')
@@ -181,7 +393,7 @@ export function formatInstructionsReport(payload, repoRoot) {
181
393
  const lines = [
182
394
  `instructions ${payload.mode}`,
183
395
  `status: ${payload.status}`,
184
- `summary: scanned=${payload.summary.scanned}, ok=${payload.summary.ok}, missing=${payload.summary.missing}, content_mismatch=${payload.summary.contentMismatch}, created=${payload.summary.created}, updated=${payload.summary.updated}, skipped=${payload.summary.skipped}`,
396
+ `summary: scanned=${payload.summary.scanned}, ok=${payload.summary.ok}, missing=${payload.summary.missing}, content_mismatch=${payload.summary.contentMismatch}, stray=${payload.summary.stray}, created=${payload.summary.created}, updated=${payload.summary.updated}, skipped=${payload.summary.skipped}`,
185
397
  ];
186
398
  if (payload.entries.length === 0) {
187
399
  lines.push('entries: none');
@@ -189,10 +401,11 @@ export function formatInstructionsReport(payload, repoRoot) {
189
401
  else {
190
402
  lines.push('entries:');
191
403
  for (const entry of payload.entries) {
192
- const agentsPath = repoRoot
193
- ? toPosixPath(relative(repoRoot, entry.agentsPath)) || '.'
194
- : toPosixPath(entry.agentsPath);
195
- lines.push(`- ${agentsPath} -> ${entry.status} (${entry.detail})`);
404
+ const displayPath = entry.agentsPath ?? entry.claudePath;
405
+ const relativePath = repoRoot
406
+ ? toPosixPath(relative(repoRoot, displayPath)) || '.'
407
+ : toPosixPath(displayPath);
408
+ lines.push(`- ${relativePath} -> ${entry.status} (${entry.detail})`);
196
409
  }
197
410
  }
198
411
  if (payload.actions.length === 0) {
@@ -1,4 +1,6 @@
1
- import type { InstructionsSyncCommandDependencies } from '../../instructions/instructions.types.js';
1
+ import { rm } from 'node:fs/promises';
2
+ import { type InstructionsSyncCommandDependencies } from '../../instructions/instructions.types.js';
2
3
  import { Command } from 'commander';
4
+ export declare function removeInstructionFile(path: string, remove?: typeof rm): Promise<void>;
3
5
  export declare function createInstructionsSyncCommand(overrides?: Partial<InstructionsSyncCommandDependencies>): Command;
4
6
  //# sourceMappingURL=sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/sync/sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,mCAAmC,EACpC,MAAM,2CAA2C,CAAC;AAUnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmHpC,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,mCAAmC,CAAM,GAC3D,OAAO,CA+DT"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/sync/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,EAAE,EAAsB,MAAM,kBAAkB,CAAC;AAI3E,OAAO,EAKL,KAAK,mCAAmC,EACzC,MAAM,2CAA2C,CAAC;AAYnD,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAQ5C,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,IAAI,CAAC,CAEf;AA+SD,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,mCAAmC,CAAM,GAC3D,OAAO,CA6FT"}
@@ -1,26 +1,96 @@
1
- import { writeFile } from 'node:fs/promises';
1
+ import { lstat, readFile, rm, symlink, writeFile } from 'node:fs/promises';
2
+ import { dirname, join, relative } from 'node:path';
2
3
  import { buildCommandContext } from '../../../app/command-context.js';
3
- import { buildInstructionsPayload, EXPECTED_CLAUDE_CONTENT, formatInstructionsReport, scanInstructionFiles, } from '../../instructions/instructions.utils.js';
4
+ import { INSTRUCTION_SYNC_STRATEGIES, } from '../../instructions/instructions.types.js';
5
+ import { buildInstructionsPayload, DEFAULT_INSTRUCTION_SYNC_STRATEGY, EXPECTED_CLAUDE_CONTENT, formatInstructionsReport, resolveInstructionSyncStrategy, scanInstructionFiles, } from '../../instructions/instructions.utils.js';
4
6
  import { readGlobalOptions } from '../../shared/shared.utils.js';
5
7
  import { CliError } from '../../../errors/cli-error.js';
6
8
  import { resolveProjectRoot } from '../../../fs/paths.js';
7
- import { Command } from 'commander';
9
+ import { Command, Option } from 'commander';
10
+ export async function removeInstructionFile(path, remove = rm) {
11
+ await remove(path, { force: true });
12
+ }
8
13
  function defaultDependencies() {
9
14
  return {
10
15
  buildCommandContext,
16
+ lstat,
17
+ readFile,
18
+ removeFile: removeInstructionFile,
11
19
  resolveProjectRoot,
12
20
  scanInstructionFiles,
21
+ symlinkFile: async (target, path) => {
22
+ await symlink(target, path, 'file');
23
+ },
13
24
  writeFile,
14
25
  };
15
26
  }
16
- function planSyncActions({ entries, force, }) {
27
+ function getSyncReason(actionType, strategy) {
28
+ const label = strategy === 'symlink'
29
+ ? 'symlink'
30
+ : strategy === 'copy'
31
+ ? 'hard copy'
32
+ : 'pointer file';
33
+ return actionType === 'create'
34
+ ? `missing CLAUDE.md ${label}`
35
+ : `overwrite CLAUDE.md with canonical ${label}`;
36
+ }
37
+ function getSyncedDetail(strategy) {
38
+ switch (strategy) {
39
+ case 'symlink':
40
+ return 'symlink synced';
41
+ case 'copy':
42
+ return 'copy synced';
43
+ default:
44
+ return 'pointer synced';
45
+ }
46
+ }
47
+ function getAgentsPath(entry) {
48
+ return entry.agentsPath ?? join(dirname(entry.claudePath), 'AGENTS.md');
49
+ }
50
+ function getErrorCode(error) {
51
+ return error && typeof error === 'object' && 'code' in error
52
+ ? String(error.code)
53
+ : null;
54
+ }
55
+ function hasUnreadableCanonicalAgents(entry) {
56
+ return (entry.agentsPath !== null &&
57
+ (entry.detail === 'broken AGENTS.md symlink' ||
58
+ entry.detail.startsWith('unreadable AGENTS.md symlink target') ||
59
+ entry.detail.startsWith('unable to read AGENTS.md')));
60
+ }
61
+ function hasUnreadableClaudeSource(entry) {
62
+ return (entry.agentsPath === null &&
63
+ (entry.detail === 'broken CLAUDE.md symlink' ||
64
+ entry.detail.startsWith('unreadable CLAUDE.md symlink target') ||
65
+ entry.detail.startsWith('unable to read CLAUDE.md')));
66
+ }
67
+ function wrapStrayResyncError(error, agentsPath, claudePath) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ return new CliError(`Adopted stray instructions into ${agentsPath}, but failed to regenerate ${claudePath}: ${message}`, error instanceof CliError ? error.exitCode : 2);
70
+ }
71
+ function planSyncActions({ entries, force, strategy, }) {
17
72
  const actions = [];
18
73
  for (const entry of entries) {
74
+ if (entry.status === 'stray') {
75
+ actions.push({
76
+ type: 'create',
77
+ target: getAgentsPath(entry),
78
+ reason: 'adopt stray CLAUDE.md into canonical AGENTS.md',
79
+ result: 'planned',
80
+ });
81
+ actions.push({
82
+ type: 'update',
83
+ target: entry.claudePath,
84
+ reason: getSyncReason('update', strategy),
85
+ result: 'planned',
86
+ });
87
+ continue;
88
+ }
19
89
  if (entry.status === 'missing') {
20
90
  actions.push({
21
91
  type: 'create',
22
92
  target: entry.claudePath,
23
- reason: 'missing CLAUDE.md pointer file',
93
+ reason: getSyncReason('create', strategy),
24
94
  result: 'planned',
25
95
  });
26
96
  continue;
@@ -28,6 +98,28 @@ function planSyncActions({ entries, force, }) {
28
98
  if (entry.status !== 'content_mismatch') {
29
99
  continue;
30
100
  }
101
+ if (hasUnreadableClaudeSource(entry)) {
102
+ actions.push({
103
+ type: 'skip',
104
+ target: entry.claudePath,
105
+ reason: 'CLAUDE.md unreadable; repair manually',
106
+ result: 'skipped',
107
+ });
108
+ continue;
109
+ }
110
+ if (hasUnreadableCanonicalAgents(entry)) {
111
+ const unreadableAgentsPath = entry.agentsPath;
112
+ if (!unreadableAgentsPath) {
113
+ throw new CliError(`Unable to resolve unreadable AGENTS.md path for ${entry.claudePath}`, 2);
114
+ }
115
+ actions.push({
116
+ type: 'skip',
117
+ target: unreadableAgentsPath,
118
+ reason: 'canonical AGENTS.md unreadable; repair manually',
119
+ result: 'skipped',
120
+ });
121
+ continue;
122
+ }
31
123
  if (!force) {
32
124
  actions.push({
33
125
  type: 'skip',
@@ -40,20 +132,76 @@ function planSyncActions({ entries, force, }) {
40
132
  actions.push({
41
133
  type: 'update',
42
134
  target: entry.claudePath,
43
- reason: 'overwrite CLAUDE.md with canonical pointer',
135
+ reason: getSyncReason('update', strategy),
44
136
  result: 'planned',
45
137
  });
46
138
  }
47
139
  return actions;
48
140
  }
49
- async function applySyncActions(actions, dependencies) {
141
+ async function applySyncActions(actions, entries, dependencies, strategy) {
50
142
  const appliedActions = [];
143
+ const entriesByTarget = new Map();
144
+ for (const entry of entries) {
145
+ entriesByTarget.set(entry.claudePath, entry);
146
+ entriesByTarget.set(getAgentsPath(entry), entry);
147
+ }
51
148
  for (const action of actions) {
52
149
  if (action.result !== 'planned') {
53
150
  appliedActions.push(action);
54
151
  continue;
55
152
  }
56
- await dependencies.writeFile(action.target, EXPECTED_CLAUDE_CONTENT, 'utf8');
153
+ const entry = entriesByTarget.get(action.target);
154
+ if (!entry) {
155
+ throw new CliError(`Unable to resolve instruction entry for ${action.target}`, 2);
156
+ }
157
+ const agentsPath = getAgentsPath(entry);
158
+ const isAgentsAction = action.target === agentsPath;
159
+ if (isAgentsAction) {
160
+ try {
161
+ await dependencies.lstat(agentsPath);
162
+ throw new CliError(`Canonical AGENTS.md appeared during sync at ${agentsPath}; re-run to reclassify before adopting stray CLAUDE.md`, 2);
163
+ }
164
+ catch (error) {
165
+ if (error instanceof CliError) {
166
+ throw error;
167
+ }
168
+ if (getErrorCode(error) !== 'ENOENT') {
169
+ throw error;
170
+ }
171
+ }
172
+ const adoptedContent = await dependencies.readFile(entry.claudePath, 'utf8');
173
+ await dependencies.writeFile(agentsPath, adoptedContent, 'utf8');
174
+ appliedActions.push({
175
+ ...action,
176
+ result: 'applied',
177
+ });
178
+ continue;
179
+ }
180
+ if (!entry.agentsPath && action.type !== 'update') {
181
+ throw new CliError(`Unable to resolve AGENTS.md for ${action.target}`, 2);
182
+ }
183
+ try {
184
+ if (action.type === 'update') {
185
+ await dependencies.removeFile(action.target);
186
+ }
187
+ if (strategy === 'symlink') {
188
+ const symlinkTarget = relative(dirname(action.target), agentsPath);
189
+ await dependencies.symlinkFile(symlinkTarget, action.target);
190
+ }
191
+ else if (strategy === 'copy') {
192
+ const agentsContent = await dependencies.readFile(agentsPath, 'utf8');
193
+ await dependencies.writeFile(action.target, agentsContent, 'utf8');
194
+ }
195
+ else {
196
+ await dependencies.writeFile(action.target, EXPECTED_CLAUDE_CONTENT, 'utf8');
197
+ }
198
+ }
199
+ catch (error) {
200
+ if (entry.status === 'stray') {
201
+ throw wrapStrayResyncError(error, agentsPath, action.target);
202
+ }
203
+ throw error;
204
+ }
57
205
  appliedActions.push({
58
206
  ...action,
59
207
  result: 'applied',
@@ -61,18 +209,21 @@ async function applySyncActions(actions, dependencies) {
61
209
  }
62
210
  return appliedActions;
63
211
  }
64
- function getPostSyncEntries(entries, actions) {
212
+ function getPostSyncEntries(entries, actions, strategy) {
65
213
  const actionByTarget = new Map(actions.map((action) => [action.target, action]));
66
214
  return entries.map((entry) => {
67
215
  const action = actionByTarget.get(entry.claudePath);
68
- if (!action) {
216
+ const adoptedAction = actionByTarget.get(getAgentsPath(entry));
217
+ if (!action && !adoptedAction) {
69
218
  return entry;
70
219
  }
71
- if (action.result === 'applied' && action.type !== 'skip') {
220
+ if (action?.result === 'applied' &&
221
+ (entry.status !== 'stray' || adoptedAction?.result === 'applied')) {
72
222
  return {
73
223
  ...entry,
224
+ agentsPath: getAgentsPath(entry),
74
225
  status: 'ok',
75
- detail: 'pointer synced',
226
+ detail: getSyncedDetail(strategy),
76
227
  };
77
228
  }
78
229
  return entry;
@@ -87,25 +238,34 @@ export function createInstructionsSyncCommand(overrides = {}) {
87
238
  ...overrides,
88
239
  };
89
240
  return new Command('sync')
90
- .description('Repair AGENTS.md to CLAUDE.md pointer drift')
241
+ .description('Repair AGENTS.md/CLAUDE.md sync drift using the selected strategy')
91
242
  .option('--dry-run', 'Preview sync changes without applying')
92
243
  .option('--force', 'Overwrite mismatched CLAUDE.md files')
244
+ .addOption(new Option('--strategy <strategy>', 'Sync strategy')
245
+ .choices([...INSTRUCTION_SYNC_STRATEGIES])
246
+ .default(DEFAULT_INSTRUCTION_SYNC_STRATEGY))
93
247
  .action(async (options, command) => {
94
248
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
95
249
  try {
96
250
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
97
- const entries = await dependencies.scanInstructionFiles(repoRoot);
251
+ const strategy = resolveInstructionSyncStrategy(options.strategy);
252
+ const entries = await dependencies.scanInstructionFiles(repoRoot, {
253
+ strategy,
254
+ });
98
255
  const plannedActions = planSyncActions({
99
256
  entries,
100
257
  force: options.force ?? false,
258
+ strategy,
101
259
  });
102
260
  const dryRun = options.dryRun ?? false;
103
261
  const actions = dryRun
104
262
  ? plannedActions
105
- : await applySyncActions(plannedActions, dependencies);
263
+ : await applySyncActions(plannedActions, entries, dependencies, strategy);
106
264
  const payload = buildInstructionsPayload({
107
265
  mode: dryRun ? 'dry-run' : 'apply',
108
- entries: dryRun ? entries : getPostSyncEntries(entries, actions),
266
+ entries: dryRun
267
+ ? entries
268
+ : getPostSyncEntries(entries, actions, strategy),
109
269
  actions,
110
270
  });
111
271
  if (context.json) {
@@ -1,4 +1,4 @@
1
- import type { InstructionsValidateCommandDependencies } from '../../instructions/instructions.types.js';
1
+ import { type InstructionsValidateCommandDependencies } from '../../instructions/instructions.types.js';
2
2
  import { Command } from 'commander';
3
3
  export declare function createInstructionsValidateCommand(overrides?: Partial<InstructionsValidateCommandDependencies>): Command;
4
4
  //# sourceMappingURL=validate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/validate/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,2CAA2C,CAAC;AASzG,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,uCAAuC,CAAM,GAC/D,OAAO,CA0CT"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/validate/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,uCAAuC,EAC7C,MAAM,2CAA2C,CAAC;AAWnD,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAU5C,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,uCAAuC,CAAM,GAC/D,OAAO,CA2DT"}
@@ -1,9 +1,10 @@
1
1
  import { buildCommandContext } from '../../../app/command-context.js';
2
- import { buildInstructionsPayload, formatInstructionsReport, scanInstructionFiles, } from '../../instructions/instructions.utils.js';
2
+ import { INSTRUCTION_SYNC_STRATEGIES, } from '../../instructions/instructions.types.js';
3
+ import { buildInstructionsPayload, DEFAULT_INSTRUCTION_SYNC_STRATEGY, formatInstructionsReport, resolveInstructionSyncStrategy, scanInstructionFiles, } from '../../instructions/instructions.utils.js';
3
4
  import { readGlobalOptions } from '../../shared/shared.utils.js';
4
5
  import { CliError } from '../../../errors/cli-error.js';
5
6
  import { resolveProjectRoot } from '../../../fs/paths.js';
6
- import { Command } from 'commander';
7
+ import { Command, Option } from 'commander';
7
8
  function defaultDependencies() {
8
9
  return {
9
10
  buildCommandContext,
@@ -17,12 +18,18 @@ export function createInstructionsValidateCommand(overrides = {}) {
17
18
  ...overrides,
18
19
  };
19
20
  return new Command('validate')
20
- .description('Validate AGENTS.md to CLAUDE.md pointer integrity')
21
- .action(async (_options, command) => {
21
+ .description('Validate AGENTS.md/CLAUDE.md sync integrity for the selected strategy')
22
+ .addOption(new Option('--strategy <strategy>', 'Sync strategy')
23
+ .choices([...INSTRUCTION_SYNC_STRATEGIES])
24
+ .default(DEFAULT_INSTRUCTION_SYNC_STRATEGY))
25
+ .action(async (options, command) => {
22
26
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
27
+ const strategy = resolveInstructionSyncStrategy(options.strategy);
23
28
  try {
24
29
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
25
- const entries = await dependencies.scanInstructionFiles(repoRoot);
30
+ const entries = await dependencies.scanInstructionFiles(repoRoot, {
31
+ strategy,
32
+ });
26
33
  const payload = buildInstructionsPayload({
27
34
  mode: 'validate',
28
35
  entries,
@@ -34,7 +41,10 @@ export function createInstructionsValidateCommand(overrides = {}) {
34
41
  else {
35
42
  context.logger.info(formatInstructionsReport(payload, repoRoot));
36
43
  if (payload.status === 'drift') {
37
- context.logger.info('Fix with: oat instructions sync');
44
+ const fixCommand = strategy === DEFAULT_INSTRUCTION_SYNC_STRATEGY
45
+ ? 'Fix with: oat instructions sync'
46
+ : `Fix with: oat instructions sync --strategy ${strategy}`;
47
+ context.logger.info(fixCommand);
38
48
  }
39
49
  }
40
50
  process.exitCode = payload.status === 'ok' ? 0 : 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
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",
@@ -33,7 +33,7 @@
33
33
  "ora": "^9.0.0",
34
34
  "yaml": "2.8.2",
35
35
  "zod": "^3.25.76",
36
- "@open-agent-toolkit/control-plane": "0.0.31"
36
+ "@open-agent-toolkit/control-plane": "0.0.32"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.0",