@mison/ling 1.0.2 → 1.1.1

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/bin/ling.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- require("./ag-kit.js");
3
+ require("./ling-cli.js");
@@ -10,6 +10,14 @@ function detectLineEnding(content) {
10
10
  }
11
11
 
12
12
  function buildMarkers(blockId) {
13
+ const id = String(blockId || "default").trim();
14
+ return {
15
+ begin: `<!-- BEGIN LING MANAGED BLOCK: ${id} -->`,
16
+ end: `<!-- END LING MANAGED BLOCK: ${id} -->`,
17
+ };
18
+ }
19
+
20
+ function buildLegacyMarkers(blockId) {
13
21
  const id = String(blockId || "default").trim();
14
22
  return {
15
23
  begin: `<!-- BEGIN AG-KIT MANAGED BLOCK: ${id} -->`,
@@ -29,10 +37,9 @@ function upsertManagedBlock(filePath, blockId, body, options = {}) {
29
37
  const lineEnding = detectLineEnding(original);
30
38
  const managedBlock = buildManagedBlock(blockId, body, lineEnding);
31
39
  const markers = buildMarkers(blockId);
32
- const blockRegex = new RegExp(
33
- `${escapeRegex(markers.begin)}[\\s\\S]*?${escapeRegex(markers.end)}`,
34
- "m",
35
- );
40
+ const legacyMarkers = buildLegacyMarkers(blockId);
41
+ const blockRegex = new RegExp(`${escapeRegex(markers.begin)}[\\s\\S]*?${escapeRegex(markers.end)}`, "m");
42
+ const legacyRegex = new RegExp(`${escapeRegex(legacyMarkers.begin)}[\\s\\S]*?${escapeRegex(legacyMarkers.end)}`, "m");
36
43
 
37
44
  let next = "";
38
45
  let action = "unchanged";
@@ -46,6 +53,12 @@ function upsertManagedBlock(filePath, blockId, body, options = {}) {
46
53
  if (next && !/\r?\n$/.test(next)) {
47
54
  next += lineEnding;
48
55
  }
56
+ } else if (legacyRegex.test(original)) {
57
+ next = original.replace(legacyRegex, managedBlock);
58
+ action = "updated";
59
+ if (next && !/\r?\n$/.test(next)) {
60
+ next += lineEnding;
61
+ }
49
62
  } else {
50
63
  next = `${original.replace(/\r?\n?$/, "")}${lineEnding}${lineEnding}${managedBlock}${lineEnding}`;
51
64
  action = "appended";
package/bin/utils.js CHANGED
@@ -1,9 +1,17 @@
1
- const { execSync } = require("child_process");
1
+ const { execSync, spawnSync } = require("child_process");
2
2
  const fs = require("fs");
3
3
  const path = require("path");
4
4
  const os = require("os");
5
5
 
6
- const REPO_URL = "https://github.com/MisonL/Ling.git";
6
+ const DEFAULT_REPO_URL = "https://github.com/MisonL/Ling.git";
7
+
8
+ function resolveRepoUrl() {
9
+ const override = process.env.LING_REPO_URL;
10
+ if (typeof override === "string" && override.trim()) {
11
+ return override.trim();
12
+ }
13
+ return DEFAULT_REPO_URL;
14
+ }
7
15
 
8
16
  function parseJsonSafe(raw) {
9
17
  try {
@@ -49,16 +57,17 @@ function cloneBranchAgentDir(branch, options) {
49
57
  throw new Error(`非法分支名: ${branch}`);
50
58
  }
51
59
 
52
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ag-kit-"));
60
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ling-"));
53
61
  const logFn = options && options.logger ? options.logger : console.log;
62
+ const repoUrl = resolveRepoUrl();
54
63
 
55
- if (!options.quiet) logFn(`[download] 正在从 ${REPO_URL} 拉取分支 ${safeBranch} ...`);
64
+ if (!options.quiet) logFn(`[download] 正在从 ${repoUrl} 拉取分支 ${safeBranch} ...`);
56
65
 
57
- try {
58
- execSync(`git clone --depth 1 --branch ${safeBranch} ${REPO_URL} "${tempDir}"`, {
59
- stdio: options.quiet ? "ignore" : "pipe",
60
- });
61
- } catch (err) {
66
+ const cloneResult = spawnSync("git", ["clone", "--depth", "1", "--branch", safeBranch, repoUrl, tempDir], {
67
+ encoding: "utf8",
68
+ stdio: options.quiet ? "ignore" : "pipe",
69
+ });
70
+ if (cloneResult.status !== 0) {
62
71
  fs.rmSync(tempDir, { recursive: true, force: true });
63
72
  throw new Error(`无法拉取分支 ${safeBranch},请确认分支存在且网络可用`);
64
73
  }
@@ -82,8 +91,42 @@ function cloneBranchAgentDir(branch, options) {
82
91
  };
83
92
  }
84
93
 
94
+ function cloneBranchSpecDir(branch, options) {
95
+ const safeBranch = branch.trim();
96
+ if (!/^[A-Za-z0-9._/-]+$/.test(safeBranch)) {
97
+ throw new Error(`非法分支名: ${branch}`);
98
+ }
99
+
100
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ling-"));
101
+ const logFn = options && options.logger ? options.logger : console.log;
102
+ const repoUrl = resolveRepoUrl();
103
+
104
+ if (!options.quiet) logFn(`[download] 正在从 ${repoUrl} 拉取分支 ${safeBranch} (spec) ...`);
105
+
106
+ const cloneResult = spawnSync("git", ["clone", "--depth", "1", "--branch", safeBranch, repoUrl, tempDir], {
107
+ encoding: "utf8",
108
+ stdio: options.quiet ? "ignore" : "pipe",
109
+ });
110
+ if (cloneResult.status !== 0) {
111
+ fs.rmSync(tempDir, { recursive: true, force: true });
112
+ throw new Error(`无法拉取分支 ${safeBranch},请确认分支存在且网络可用`);
113
+ }
114
+
115
+ const specDir = path.join(tempDir, ".spec");
116
+ if (!fs.existsSync(specDir)) {
117
+ fs.rmSync(tempDir, { recursive: true, force: true });
118
+ throw new Error(`分支 ${safeBranch} 中未找到 .spec 目录`);
119
+ }
120
+
121
+ return {
122
+ specDir,
123
+ cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
124
+ };
125
+ }
126
+
85
127
  module.exports = {
86
128
  parseJsonSafe,
87
129
  readGlobalNpmDependencies,
88
- cloneBranchAgentDir
130
+ cloneBranchAgentDir,
131
+ cloneBranchSpecDir
89
132
  };
package/docs/TECH.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  ## 快速验证(维护者)
4
4
  ```bash
5
- bun install
6
- bun run test
7
- bun run ci:verify
8
- bun run health-check
9
- cd web && bun install && bun run lint
5
+ npm install
6
+ npm test
7
+ npm run ci:verify
8
+ npm run health-check
9
+ cd web && npm install && npm run lint
10
10
  ```
11
11
 
12
12
  ## 核心目录与职责
@@ -20,6 +20,9 @@ cd web && bun install && bun run lint
20
20
  ### 项目级(功能最完整)
21
21
  - `gemini`:项目根目录 `.agent/`
22
22
  - `codex`:项目根目录 `.agents/`(受管)+ `.agents-backup/`(漂移覆盖备份)
23
+ - 项目级预备份(覆盖前快照):
24
+ - Gemini:`<project>/.agent-backup/<timestamp>/preflight/.agent/`
25
+ - Codex:`<project>/.agents-backup/<timestamp>/preflight/.agents/` 或 `<project>/.agents-backup/<timestamp>/preflight/.codex/`
23
26
 
24
27
  ### 全局级(仅同步 Skills)
25
28
  - `codex`:`$HOME/.codex/skills/`
@@ -31,9 +34,16 @@ cd web && bun install && bun run lint
31
34
  ## 端到端链路(简述)
32
35
  ### 项目安装 / 更新
33
36
  - `init`:选择目标 -> 适配器 `install()` -> 落盘目标目录(Gemini: `.agent/`;Codex: `.agents/`)->(Codex)注入托管区块到工作区 `AGENTS.md` 与 `ling.rules`
34
- - `update`:自动检测已安装目标(或通过 `--target/--targets` 指定)-> 适配器 `update()` ->(Codex)漂移检测与备份 -> 原子替换
37
+ - `update`:自动检测已安装目标(或通过 `--target/--targets` 指定)->(冲突时交互确认或默认预备份)-> 适配器 `update()` ->(Codex)漂移检测与备份 -> 原子替换
35
38
  - `doctor`:检查完整性;`--fix` 尝试修复(Codex 支持迁移 `.codex/` 与重写托管区块)
36
39
 
40
+ ### 已有资产冲突处理(项目级)
41
+ - 触发条件:
42
+ - `gemini`:`.agent/` 已存在且与内置模板不一致
43
+ - `codex`:`.agents/` 或 `.codex/` 已存在且存在漂移、缺失 `manifest.json` 或包含未知文件
44
+ - 交互终端会逐项询问处理方式:保留 / 备份后移除 / 直接移除,并支持按资产类别复用选择
45
+ - 非交互环境不会进入询问:需要覆盖时默认执行“备份后覆盖”;`init` 若检测到已有资产且未显式 `--force` 则报错
46
+
37
47
  ### Codex 构建(Workflow -> Skill)
38
48
  - 输入:`.agents/skills/` 与 `.agents/workflows/`
39
49
  - 规则:每个 Workflow `<name>.md` 会转换为一个 Skill:`workflow-<name>/SKILL.md`
@@ -55,19 +65,27 @@ cd web && bun install && bun run lint
55
65
  ### 测试隔离
56
66
  - `LING_GLOBAL_ROOT`:替代 `$HOME`(用于测试与 CI,避免污染真实用户目录)
57
67
 
58
- ## Spec Profile:`ling spec enable/disable/status`
68
+ ## Spec Profile:`ling spec ...`
59
69
  ### 当前范围
60
- - 当前只实现全局层:
70
+ - 全局 Spec Profile(机器级):
61
71
  - `ling spec enable [--target codex|gemini] [--dry-run] [--quiet]`
62
72
  - `ling spec disable [--target codex|gemini] [--dry-run] [--quiet]`
63
73
  - `ling spec status [--quiet]`
64
- - 默认目标:未指定 `--target/--targets` 时启用 `codex + gemini`
65
- - 当前 Spec 源目录:`.spec/`
74
+ - 默认目标:未指定 `--target/--targets` 时启用 `codex + gemini`
75
+ - 项目级 Spec 资产(工作区级):
76
+ - `ling spec init [--path <dir>] [--target codex|gemini|--targets codex,gemini] [--branch <name>] [--dry-run] [--quiet]`
77
+ - 未指定 `--path` 时,默认初始化 Spec 工作区目录:`$HOME/.ling/spec-workspace`
78
+ - 指定 `--targets` 时,会同时执行对应目标的 `ling init` 安装(`.agent/.agents`)
79
+ - 指定 `--branch` 时,Spec 资产将从该分支的 `.spec/` 拉取(用于验证或灰度 Spec 模板)
80
+ - `ling spec doctor [--path <dir>] [--quiet]`
81
+ - 会校验 `issues.csv` 表头与状态枚举,并检查 `.ling/spec/` 目录是否完整
82
+ - Spec 源目录:`.spec/`
66
83
 
67
84
  ### 落盘与状态
68
85
  - Spec 状态文件:`$HOME/.ling/spec/state.json`
69
86
  - Spec templates:`$HOME/.ling/spec/templates/`
70
87
  - Spec references:`$HOME/.ling/spec/references/`
88
+ - Spec profiles:`$HOME/.ling/spec/profiles/`
71
89
  - Spec 备份目录:`$HOME/.ling/backups/spec/<timestamp>/before/...`
72
90
 
73
91
  ### 当前安装内容
@@ -84,6 +102,10 @@ cd web && bun install && bun run lint
84
102
  - `harness-engineering-digest.md`
85
103
  - `gda-framework.md`
86
104
  - 相关 quickstart / README
105
+ - Profiles:
106
+ - `codex/AGENTS.spec.md`
107
+ - `codex/ling.spec.rules.md`
108
+ - `gemini/GEMINI.spec.md`
87
109
 
88
110
  ### 状态契约
89
111
  - `ling spec status --quiet` 输出:
@@ -98,11 +120,13 @@ cd web && bun install && bun run lint
98
120
  ### 回退语义
99
121
  - `spec enable`:
100
122
  - 若目标位置已存在同名 Skill,会先备份再覆盖
101
- - 若 `templates/` 或 `references/` 已存在,也会先备份
123
+ - 若 `templates/`、`references/` 或 `profiles/` 已存在,也会先备份
102
124
  - `spec disable`:
103
125
  - 若存在备份,恢复启用前快照
104
126
  - 若启用前不存在资源,则删除由 Spec 安装的目录
105
- - 当前尚未实现项目级 `spec init / remove / doctor`
127
+ - `spec init`:
128
+ - 会在工作区内写入 `.ling/spec/`(templates/references/profiles)与 `issues.csv`,并创建 `docs/reviews`、`docs/handoff`
129
+ - 当检测到冲突资产时,支持保留 / 备份后覆盖 / 直接覆盖
106
130
 
107
131
  ## 状态契约(自动化)
108
132
  - `ling status --quiet` / `ling global status --quiet` 只输出三态:
@@ -158,10 +182,10 @@ cp -a "$HOME/.ling/backups/global/$ts/antigravity/$skill" "$HOME/.gemini/antigra
158
182
  - `LING_INDEX_PATH`:工作区索引文件路径(默认 `~/.ling/workspaces.json`)
159
183
  - `LING_GLOBAL_ROOT`:全局目录根(替代 `$HOME`)
160
184
  - `LING_SKIP_UPSTREAM_CHECK`:跳过上游同名包安装提示(测试用)
185
+ - `LING_REPO_URL`:覆盖默认仓库地址(主要用于测试与私有镜像)
161
186
 
162
187
  ## 安装提示机制
163
188
  - npm 全局安装:`postinstall` 会尽力检测并提示上游英文版 `@vudovn/ag-kit` 冲突。
164
- - Bun 全局安装:Bun 默认会阻止本包 `postinstall`;因此冲突提示以内置 CLI 运行期检查为准,会在 `init` / `update` / `update-all` / `global sync` 时提示。
165
189
  - 冲突提示只负责提醒,不会自动修改当前安装状态;如需清理可执行 `npm uninstall -g @vudovn/ag-kit`。
166
190
 
167
191
  ## 常见故障
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mison/ling",
3
- "version": "1.0.2",
4
- "description": "AI Agent templates - Skills, Agents, and Workflows for enhanced coding assistance",
3
+ "version": "1.1.1",
4
+ "description": "面向 Gemini CLI、Antigravity Codex 的中文 AI Agent 模板工具包",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/MisonL/Ling.git"
@@ -18,7 +18,10 @@
18
18
  "skills",
19
19
  "templates"
20
20
  ],
21
- "author": "vudovn",
21
+ "author": "Mison",
22
+ "contributors": [
23
+ "vudovn"
24
+ ],
22
25
  "license": "MIT",
23
26
  "scripts": {
24
27
  "clean": "node scripts/clean.js",
@@ -9,6 +9,7 @@ const REPO_ROOT = path.resolve(__dirname, "..");
9
9
  const CLI_PATH = path.join(REPO_ROOT, "bin", "ling.js");
10
10
 
11
11
  function runCli(args, options = {}) {
12
+ const allowedStatuses = options.allowedStatuses || [0];
12
13
  const env = {
13
14
  ...process.env,
14
15
  LING_SKIP_UPSTREAM_CHECK: "1",
@@ -21,9 +22,11 @@ function runCli(args, options = {}) {
21
22
  encoding: "utf8",
22
23
  });
23
24
 
24
- if (result.status !== 0) {
25
+ if (!allowedStatuses.includes(result.status)) {
25
26
  const message = result.stderr || result.stdout || "";
26
- throw new Error(`命令失败: ling ${args.join(" ")}\n${message}`);
27
+ throw new Error(
28
+ `命令失败: ling ${args.join(" ")}\n(exit=${result.status})\n${message}`.trim(),
29
+ );
27
30
  }
28
31
 
29
32
  return result.stdout || "";
@@ -82,7 +85,10 @@ function main() {
82
85
  throw new Error(`spec status 结果异常: ${specStatus}`);
83
86
  }
84
87
  runCli(["spec", "disable", "--target", "codex", "--quiet"], { env });
85
- const specStatusAfterDisable = runCli(["spec", "status", "--quiet"], { env }).trim();
88
+ const specStatusAfterDisable = runCli(["spec", "status", "--quiet"], {
89
+ env,
90
+ allowedStatuses: [0, 2],
91
+ }).trim();
86
92
  if (specStatusAfterDisable !== "missing") {
87
93
  throw new Error(`spec disable 后状态异常: ${specStatusAfterDisable}`);
88
94
  }
@@ -58,7 +58,7 @@ async function main() {
58
58
  return;
59
59
  }
60
60
 
61
- if (process.env.LING_SKIP_UPSTREAM_CHECK === "1" || process.env.AG_KIT_SKIP_UPSTREAM_CHECK === "1") {
61
+ if (process.env.LING_SKIP_UPSTREAM_CHECK === "1") {
62
62
  return;
63
63
  }
64
64
 
@@ -72,13 +72,12 @@ async function main() {
72
72
  }
73
73
 
74
74
  console.warn(`\n[warn] 检测到全局已安装上游英文版 ${UPSTREAM_GLOBAL_PACKAGE}`);
75
- console.warn("[warn] 上游英文版与当前版本共用 `ag-kit` 兼容命令名,后安装者会覆盖该入口。");
76
- console.warn("[warn] 为避免后续混淆,建议仅保留一个来源。\n");
75
+ console.warn("[warn] 为避免 Skills/模板来源混用导致的行为差异,建议仅保留一个来源。\n");
77
76
 
78
77
  if (!canPromptUser()) {
79
78
  console.warn("[info] 当前环境不是交互式终端,无法确认是否自动卸载。");
80
79
  console.warn(`[hint] 如需卸载,请手动执行: npm uninstall -g ${UPSTREAM_GLOBAL_PACKAGE}`);
81
- console.warn("[info] 本次将继续安装;安装完成后正式命令请使用 `ling`。\n");
80
+ console.warn("[info] 本次将继续安装;安装完成后请使用 `ling`。\n");
82
81
  return;
83
82
  }
84
83
 
@@ -86,7 +85,7 @@ async function main() {
86
85
 
87
86
  if (!shouldUninstall) {
88
87
  console.warn(`[info] 已保留 ${UPSTREAM_GLOBAL_PACKAGE},继续安装当前版本。`);
89
- console.warn("[info] 结果说明:正式命令请使用 `ling`,旧 `ag-kit` 仅保留兼容入口。\n");
88
+ console.warn("[info] 结果说明:安装完成后请使用 `ling`。\n");
90
89
  return;
91
90
  }
92
91
 
@@ -101,7 +100,7 @@ async function main() {
101
100
  console.warn(`[error] 自动卸载 ${UPSTREAM_GLOBAL_PACKAGE} 失败,将继续安装当前版本。`);
102
101
  console.warn("[info] 若需手动处理,请执行:");
103
102
  console.warn(` npm uninstall -g ${UPSTREAM_GLOBAL_PACKAGE}`);
104
- console.warn("[info] 安装完成后,正式命令请使用 `ling`;旧 `ag-kit` 仅保留兼容入口。\n");
103
+ console.warn("[info] 安装完成后请使用 `ling`。\n");
105
104
  }
106
105
 
107
106
  main().catch((err) => {
@@ -9,7 +9,7 @@ describe("Clean Script", () => {
9
9
  let tempRoot;
10
10
 
11
11
  beforeEach(() => {
12
- tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ag-kit-clean-test-"));
12
+ tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ling-clean-test-"));
13
13
  });
14
14
 
15
15
  afterEach(() => {
@@ -27,6 +27,37 @@ describe("CLI Smoke", () => {
27
27
  let workspaceDir;
28
28
  let indexPath;
29
29
 
30
+ function findFirstTimestampDir(backupRoot) {
31
+ if (!fs.existsSync(backupRoot)) {
32
+ return "";
33
+ }
34
+ const entries = fs
35
+ .readdirSync(backupRoot, { withFileTypes: true })
36
+ .filter((entry) => entry.isDirectory())
37
+ .map((entry) => entry.name)
38
+ .sort();
39
+ return entries.length > 0 ? path.join(backupRoot, entries[entries.length - 1]) : "";
40
+ }
41
+
42
+ function findTimestampDirContaining(backupRoot, relPath) {
43
+ if (!fs.existsSync(backupRoot)) {
44
+ return "";
45
+ }
46
+ const entries = fs
47
+ .readdirSync(backupRoot, { withFileTypes: true })
48
+ .filter((entry) => entry.isDirectory())
49
+ .map((entry) => entry.name)
50
+ .sort();
51
+
52
+ for (const name of entries) {
53
+ const candidate = path.join(backupRoot, name, relPath);
54
+ if (fs.existsSync(candidate)) {
55
+ return path.join(backupRoot, name);
56
+ }
57
+ }
58
+ return "";
59
+ }
60
+
30
61
  beforeEach(() => {
31
62
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ling-cli-test-"));
32
63
  workspaceDir = path.join(tempDir, "workspace");
@@ -273,6 +304,60 @@ describe("CLI Smoke", () => {
273
304
  assert.strictEqual(result.status, 0, result.stderr || result.stdout);
274
305
  });
275
306
 
307
+ test("init --force should create preflight backup for existing gemini .agent", () => {
308
+ const agentDir = path.join(workspaceDir, ".agent");
309
+ fs.mkdirSync(agentDir, { recursive: true });
310
+ fs.writeFileSync(path.join(agentDir, "custom.txt"), "custom", "utf8");
311
+
312
+ const result = runCli(
313
+ ["init", "--target", "gemini", "--path", workspaceDir, "--force", "--quiet"],
314
+ { env: { LING_INDEX_PATH: indexPath } },
315
+ );
316
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
317
+
318
+ const backupRoot = path.join(workspaceDir, ".agent-backup");
319
+ const tsDir = findFirstTimestampDir(backupRoot);
320
+ assert.ok(tsDir, "expected .agent-backup timestamp directory to exist");
321
+ assert.ok(fs.existsSync(path.join(tsDir, "preflight", ".agent", "custom.txt")));
322
+ });
323
+
324
+ test("update should create preflight backup for gemini when overwriting in non-interactive mode", () => {
325
+ const agentDir = path.join(workspaceDir, ".agent");
326
+ fs.mkdirSync(agentDir, { recursive: true });
327
+ fs.writeFileSync(path.join(agentDir, "custom.txt"), "custom", "utf8");
328
+
329
+ const result = runCli(
330
+ ["update", "--target", "gemini", "--path", workspaceDir, "--quiet"],
331
+ { env: { LING_INDEX_PATH: indexPath } },
332
+ );
333
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
334
+
335
+ const backupRoot = path.join(workspaceDir, ".agent-backup");
336
+ const tsDir = findFirstTimestampDir(backupRoot);
337
+ assert.ok(tsDir, "expected .agent-backup timestamp directory to exist");
338
+ assert.ok(fs.existsSync(path.join(tsDir, "preflight", ".agent", "custom.txt")));
339
+ });
340
+
341
+ test("update should create preflight backup for codex when unknown files exist in managed dir", () => {
342
+ const initResult = runCli(
343
+ ["init", "--target", "codex", "--path", workspaceDir, "--quiet"],
344
+ { env: { LING_INDEX_PATH: indexPath } },
345
+ );
346
+ assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
347
+
348
+ fs.writeFileSync(path.join(workspaceDir, ".agents", "unknown.txt"), "unknown", "utf8");
349
+
350
+ const updateResult = runCli(
351
+ ["update", "--target", "codex", "--path", workspaceDir, "--quiet"],
352
+ { env: { LING_INDEX_PATH: indexPath } },
353
+ );
354
+ assert.strictEqual(updateResult.status, 0, updateResult.stderr || updateResult.stdout);
355
+
356
+ const backupRoot = path.join(workspaceDir, ".agents-backup");
357
+ const tsDir = findTimestampDirContaining(backupRoot, path.join("preflight", ".agents", "unknown.txt"));
358
+ assert.ok(tsDir, "expected .agents-backup preflight backup to exist");
359
+ });
360
+
276
361
  test("status --quiet should report missing with exit code 2 when nothing is installed", () => {
277
362
  const result = runCli(
278
363
  ["status", "--path", workspaceDir, "--quiet"],
@@ -25,7 +25,7 @@ describe("ManagedBlock", () => {
25
25
  assert.strictEqual(result.action, "appended");
26
26
  const content = fs.readFileSync(targetFile, "utf8");
27
27
  assert.ok(content.includes("User Content"));
28
- assert.ok(content.includes("BEGIN AG-KIT MANAGED BLOCK: codex-core-rules"));
28
+ assert.ok(content.includes("BEGIN LING MANAGED BLOCK: codex-core-rules"));
29
29
  assert.ok(content.includes("managed line"));
30
30
  });
31
31
 
@@ -38,4 +38,21 @@ describe("ManagedBlock", () => {
38
38
  assert.ok(content.includes("v2"));
39
39
  assert.ok(!content.includes("v1"));
40
40
  });
41
+
42
+ test("should migrate legacy AG-KIT markers when updating managed block", () => {
43
+ fs.writeFileSync(
44
+ targetFile,
45
+ `# User Content\n\n<!-- BEGIN AG-KIT MANAGED BLOCK: codex-core-rules -->\nlegacy\n<!-- END AG-KIT MANAGED BLOCK: codex-core-rules -->\n`,
46
+ "utf8",
47
+ );
48
+
49
+ const result = upsertManagedBlock(targetFile, "codex-core-rules", "v2");
50
+
51
+ assert.strictEqual(result.action, "updated");
52
+ const content = fs.readFileSync(targetFile, "utf8");
53
+ assert.ok(content.includes("BEGIN LING MANAGED BLOCK: codex-core-rules"));
54
+ assert.ok(!content.includes("BEGIN AG-KIT MANAGED BLOCK: codex-core-rules"));
55
+ assert.ok(content.includes("v2"));
56
+ assert.ok(!content.includes("legacy"));
57
+ });
41
58
  });
@@ -66,9 +66,9 @@ describe('Phase C Integration', () => {
66
66
  // Verification 4: Workspace managed block injection
67
67
  const workspaceAgents = fs.readFileSync(path.join(workDir, 'AGENTS.md'), 'utf8');
68
68
  const workspaceRules = fs.readFileSync(path.join(workDir, 'ling.rules'), 'utf8');
69
- assert.ok(workspaceAgents.includes('BEGIN AG-KIT MANAGED BLOCK: codex-core-rules'));
69
+ assert.ok(workspaceAgents.includes('BEGIN LING MANAGED BLOCK: codex-core-rules'));
70
70
  assert.ok(workspaceAgents.includes('test-skill'));
71
- assert.ok(workspaceRules.includes('BEGIN AG-KIT MANAGED BLOCK: codex-risk-controls'));
71
+ assert.ok(workspaceRules.includes('BEGIN LING MANAGED BLOCK: codex-risk-controls'));
72
72
 
73
73
  const codexJson = JSON.parse(fs.readFileSync(path.join(codexDir, 'codex.json'), 'utf8'));
74
74
  assert.strictEqual(codexJson.version, pkg.version);