@mison/ling 1.1.1 → 1.2.0

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/docs/PLAN.md CHANGED
@@ -12,15 +12,17 @@
12
12
  ### 项目安装(功能最完整)
13
13
  - 命令:`ling init` / `ling update` / `ling status` / `ling doctor`
14
14
  - 目标输出:
15
- - `gemini` -> 项目内 `.agent/`(兼容 Gemini/Antigravity 工作区规范)
15
+ - `gemini` -> 项目内 `.agent/`
16
+ - `antigravity` -> 项目内 `.agent/`(与 Gemini 复用工作区目录,但命令与注册独立)
16
17
  - `codex` -> 项目内 `.agents/`(受管目录)+ 注入工作区 `AGENTS.md` / `ling.rules` 托管区块
17
18
 
18
19
  ### 全局安装(跨项目复用 Skills)
19
20
  - 命令:`ling global sync` / `ling global status`
20
- - 默认行为:`ling global sync` 未指定 `--target/--targets` 时,同步 `codex + gemini`
21
+ - 默认行为:`ling global sync` 未指定 `--target/--targets` 时,同步 `codex + gemini + antigravity`
21
22
  - 目标路径:
22
23
  - `codex` -> `$HOME/.codex/skills/`
23
- - `gemini` -> 同时写入 `$HOME/.gemini/skills/` 与 `$HOME/.gemini/antigravity/skills/`
24
+ - `gemini` -> `$HOME/.gemini/skills/`
25
+ - `antigravity` -> `$HOME/.gemini/antigravity/skills/`
24
26
  - 安全边界:全局只同步 Skills,不写入全局 Rules/Agents/Workflows。
25
27
 
26
28
  ## 覆盖与回滚(全局同步)
@@ -28,27 +30,29 @@
28
30
  - 覆盖策略:只覆盖同名 Skill;不清理用户已有的其他 Skill。
29
31
  - 覆盖前备份:每次覆盖同名 Skill 前备份到 `$HOME/.ling/backups/global/<timestamp>/...`。
30
32
 
31
- ## Spec Profile(可选进阶层)
33
+ ## Spec(核心进阶层)
32
34
  ### 当前阶段已落地
33
- - 命令:`ling spec enable` / `ling spec disable` / `ling spec status`
34
35
  - 默认关闭,必须显式启用
35
- - 当前只开放全局层,不进入项目级注入
36
- - 目标资源:
37
- - Skills:`harness-engineering`、`cybernetic-systems-engineering`
38
- - Templates:`$HOME/.ling/spec/templates/`
39
- - References:`$HOME/.ling/spec/references/`
40
- - 回退原则:启用前先备份同名资源;停用时优先恢复原资源
41
-
42
- ### 后续阶段
43
- - 项目级 `spec init / remove / doctor`
44
- - `.spec/profiles/` 的项目投影与可逆注入
36
+ - 全局层(机器级)命令:`ling spec enable` / `ling spec disable` / `ling spec status`
37
+ - 目标资源:
38
+ - Skills:`harness-engineering`、`cybernetic-systems-engineering`
39
+ - Templates:`$HOME/.ling/spec/templates/`
40
+ - References:`$HOME/.ling/spec/references/`
41
+ - Profiles:`$HOME/.ling/spec/profiles/`
42
+ - 回退原则:启用前先备份同名资源;停用时优先恢复原资源
43
+ - 项目层(工作区级)命令:`ling spec init` / `ling spec doctor`
44
+ - 默认作用于当前目录;`--spec-workspace` 才会使用 `$HOME/.ling/spec-workspace`
45
+ - 完整模式:写入 `.ling/spec/`、`issues.csv`、`docs/reviews/`、`docs/handoff/`
46
+ - 轻量模式:`--csv-only` 只写入 `issues.csv` 与 `docs/*`,项目内不落地 `.ling/spec/`
47
+ - 可选:通过 `--target/--targets` 同时安装 `.agent/.agents`
45
48
 
46
49
  ## 兼容策略
47
- - Gemini/Antigravity:输出 `.agent/`,保持与官方工作区机制一致。
50
+ - Gemini/Antigravity:项目级继续输出 `.agent/`,保持与官方工作区机制一致;命令、状态、索引按独立目标处理。
51
+ - 为避免共享 `.agent/` 丢失逻辑目标身份,工作区额外维护 `.ling/install-state.json`。
48
52
  - Codex:受管目录为 `.agents/`,并使用 `manifest.json` 做完整性与漂移检测;识别并迁移遗留 `.codex/`。
49
53
  - 全局同步遵循真实消费端目录,而不是仓库模板源目录;仓库内仍以 `.agents/` 作为唯一 Canonical。
50
54
 
51
55
  ## 成功标准
52
- - `ling global sync` 一条命令即可完成全局 Skills 安装/更新(默认 codex + gemini,其中 gemini 同步到 gemini-cli 与 antigravity)。
56
+ - `ling global sync` 一条命令即可完成全局 Skills 安装/更新(默认 codex + gemini + antigravity)。
53
57
  - 覆盖可回滚:每次覆盖同名 Skill 都有可用备份。
54
58
  - 跨平台 CI(Linux/macOS/Windows)验证主链路通过。
package/docs/TECH.md CHANGED
@@ -12,14 +12,16 @@ cd web && npm install && npm run lint
12
12
  ## 核心目录与职责
13
13
  - `.agents/`:仓库模板源(Canonical)
14
14
  - `bin/ling.js`:CLI 入口与命令分发
15
- - `bin/adapters/`:目标差异(`gemini` / `codex`)
15
+ - `bin/adapters/`:目标差异(`gemini` / `antigravity` / `codex`)
16
16
  - `bin/core/`:构建/转换(将 Workflows 投影为 Codex Skills 等)
17
17
  - `bin/utils/`:原子写入、manifest、托管区块等通用能力
18
18
 
19
19
  ## 路径映射(最重要)
20
20
  ### 项目级(功能最完整)
21
21
  - `gemini`:项目根目录 `.agent/`
22
+ - `antigravity`:项目根目录 `.agent/`(与 Gemini 复用目录,但索引与状态独立)
22
23
  - `codex`:项目根目录 `.agents/`(受管)+ `.agents-backup/`(漂移覆盖备份)
24
+ - 工作区安装状态:共享 `.agent/` 目标时写入项目根目录 `.ling/install-state.json`(记录逻辑目标注册信息)
23
25
  - 项目级预备份(覆盖前快照):
24
26
  - Gemini:`<project>/.agent-backup/<timestamp>/preflight/.agent/`
25
27
  - Codex:`<project>/.agents-backup/<timestamp>/preflight/.agents/` 或 `<project>/.agents-backup/<timestamp>/preflight/.codex/`
@@ -33,13 +35,14 @@ cd web && npm install && npm run lint
33
35
 
34
36
  ## 端到端链路(简述)
35
37
  ### 项目安装 / 更新
36
- - `init`:选择目标 -> 适配器 `install()` -> 落盘目标目录(Gemini: `.agent/`;Codex: `.agents/`)->(Codex)注入托管区块到工作区 `AGENTS.md` 与 `ling.rules`
38
+ - `init`:选择目标 -> 适配器 `install()` -> 落盘目标目录(Gemini/Antigravity: `.agent/`;Codex: `.agents/`)->(Codex)注入托管区块到工作区 `AGENTS.md` 与 `ling.rules`
37
39
  - `update`:自动检测已安装目标(或通过 `--target/--targets` 指定)->(冲突时交互确认或默认预备份)-> 适配器 `update()` ->(Codex)漂移检测与备份 -> 原子替换
40
+ - 共享 `.agent/` 的 `gemini` / `antigravity` 身份优先从 `.ling/install-state.json` 读取;若缺失则回退到工作区索引,再回退为历史默认 `gemini`
38
41
  - `doctor`:检查完整性;`--fix` 尝试修复(Codex 支持迁移 `.codex/` 与重写托管区块)
39
42
 
40
43
  ### 已有资产冲突处理(项目级)
41
44
  - 触发条件:
42
- - `gemini`:`.agent/` 已存在且与内置模板不一致
45
+ - `gemini` / `antigravity`:`.agent/` 已存在且与内置模板不一致
43
46
  - `codex`:`.agents/` 或 `.codex/` 已存在且存在漂移、缺失 `manifest.json` 或包含未知文件
44
47
  - 交互终端会逐项询问处理方式:保留 / 备份后移除 / 直接移除,并支持按资产类别复用选择
45
48
  - 非交互环境不会进入询问:需要覆盖时默认执行“备份后覆盖”;`init` 若检测到已有资产且未显式 `--force` 则报错
@@ -51,8 +54,8 @@ cd web && npm install && npm run lint
51
54
 
52
55
  ## 全局同步:`ling global sync/status`
53
56
  ### 默认目标
54
- - 未指定 `--target/--targets`:默认同步 `codex + gemini`
55
- - `--target gemini` / `--targets codex,gemini` 中的 `gemini` 会同时写入 gemini-cli 与 antigravity 两个消费端目录
57
+ - 未指定 `--target/--targets`:默认同步 `codex + gemini + antigravity`
58
+ - `--target gemini` 只写入 gemini-cli;`--target antigravity` 只写入 antigravity
56
59
 
57
60
  ### 来源与覆盖策略
58
61
  - 来源:默认使用本包内置 `.agents/`;也可用 `--branch <name>` 从远端分支拉取模板源
@@ -65,22 +68,31 @@ cd web && npm install && npm run lint
65
68
  ### 测试隔离
66
69
  - `LING_GLOBAL_ROOT`:替代 `$HOME`(用于测试与 CI,避免污染真实用户目录)
67
70
 
68
- ## Spec Profile:`ling spec ...`
71
+ ## Spec:`ling spec ...`
69
72
  ### 当前范围
70
73
  - 全局 Spec Profile(机器级):
71
- - `ling spec enable [--target codex|gemini] [--dry-run] [--quiet]`
72
- - `ling spec disable [--target codex|gemini] [--dry-run] [--quiet]`
74
+ - `ling spec enable [--target codex|gemini|antigravity] [--dry-run] [--quiet]`
75
+ - `ling spec disable [--target codex|gemini|antigravity] [--dry-run] [--quiet]`
73
76
  - `ling spec status [--quiet]`
74
- - 默认目标:未指定 `--target/--targets` 时启用 `codex + gemini`
77
+ - 默认目标:未指定 `--target/--targets` 时启用 `codex + gemini + antigravity`
75
78
  - 项目级 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`
79
+ - `ling spec init [--path <dir>] [--spec-workspace] [--csv-only] [--target codex|gemini|antigravity|--targets codex,gemini,antigravity] [--branch <name>] [--dry-run] [--quiet]`
80
+ - 默认初始化当前目录
81
+ - `--path <dir>`:初始化指定项目目录
82
+ - `--spec-workspace`:初始化 `$HOME/.ling/spec-workspace`(本机独立演练工作区,默认会包含 `.agent/.agents`)
78
83
  - 指定 `--targets` 时,会同时执行对应目标的 `ling init` 安装(`.agent/.agents`)
79
84
  - 指定 `--branch` 时,Spec 资产将从该分支的 `.spec/` 拉取(用于验证或灰度 Spec 模板)
80
- - `ling spec doctor [--path <dir>] [--quiet]`
81
- - 会校验 `issues.csv` 表头与状态枚举,并检查 `.ling/spec/` 目录是否完整
85
+ - `--csv-only`:仅生成 `issues.csv` `docs/*`,不写入 `.ling/spec/`(适用于依赖全局 Spec 模板的项目)
86
+ - `ling spec doctor [--path <dir>] [--spec-workspace] [--quiet]`
87
+ - 会校验 `issues.csv` 表头与状态枚举
88
+ - 当项目缺少 `.ling/spec/` 但已启用全局 Spec 时,会使用全局 Spec 资产作为后备
82
89
  - Spec 源目录:`.spec/`
83
90
 
91
+ ### 心智模型
92
+ - `ling spec enable`:给这台电脑安装 Spec 工具箱
93
+ - `ling spec init`:给当前项目创建真正要用的 `issues.csv`
94
+ - `issues.csv` 永远在项目根目录;全局 Spec 不保存项目任务
95
+
84
96
  ### 落盘与状态
85
97
  - Spec 状态文件:`$HOME/.ling/spec/state.json`
86
98
  - Spec templates:`$HOME/.ling/spec/templates/`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mison/ling",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "面向 Gemini CLI、Antigravity 与 Codex 的中文 AI Agent 模板工具包",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,13 +51,16 @@ function main() {
51
51
  LING_GLOBAL_ROOT: globalRoot,
52
52
  };
53
53
 
54
- runCli(["init", "--targets", "gemini,codex", "--path", workspaceDir, "--quiet"], { env });
54
+ runCli(["init", "--targets", "gemini,antigravity,codex", "--path", workspaceDir, "--quiet"], { env });
55
55
 
56
56
  const status = runCli(["status", "--path", workspaceDir, "--quiet"], { env }).trim();
57
57
  if (status !== "installed") {
58
58
  throw new Error(`status 结果异常: ${status}`);
59
59
  }
60
60
 
61
+ const installStatePath = path.join(workspaceDir, ".ling", "install-state.json");
62
+ ensureExists(installStatePath, "工作区安装状态文件");
63
+
61
64
  runCli(["doctor", "--path", workspaceDir, "--quiet"], { env });
62
65
  runCli(["update", "--path", workspaceDir, "--quiet"], { env });
63
66
  runCli(["update-all", "--dry-run", "--quiet"], { env });
@@ -48,17 +48,12 @@ function main() {
48
48
  throw new Error("缺少命令: node");
49
49
  }
50
50
 
51
- const packageRunner = commandExists("bun --version") ? "bun" : "npm";
52
- if (!commandExists(`${packageRunner} --version`)) {
53
- throw new Error(`缺少命令: ${packageRunner}`);
51
+ if (!commandExists("npm --version")) {
52
+ throw new Error("缺少命令: npm");
54
53
  }
55
54
 
56
55
  logStep("执行测试套件");
57
- if (packageRunner === "bun") {
58
- runCommand("bun run test");
59
- } else {
60
- runCommand("npm test --silent");
61
- }
56
+ runCommand("npm test --silent");
62
57
 
63
58
  logStep("验证 CLI 核心链路");
64
59
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ling-health-check-"));
@@ -132,11 +127,7 @@ function main() {
132
127
  }
133
128
 
134
129
  logStep("执行清理预检");
135
- if (packageRunner === "bun") {
136
- runCommand("bun run clean:dry-run");
137
- } else {
138
- runCommand("npm run clean:dry-run --silent");
139
- }
130
+ runCommand("npm run clean:dry-run --silent");
140
131
 
141
132
  console.log("[ok] 健康检查通过");
142
133
  }
@@ -338,6 +338,78 @@ describe("CLI Smoke", () => {
338
338
  assert.ok(fs.existsSync(path.join(tsDir, "preflight", ".agent", "custom.txt")));
339
339
  });
340
340
 
341
+ test("init should allow registering antigravity independently on shared .agent", () => {
342
+ const localWorkspace = fs.mkdtempSync(path.join(REPO_ROOT, ".tmp-ling-antigravity-init-"));
343
+ try {
344
+ const result = runCli(
345
+ ["init", "--target", "antigravity", "--path", localWorkspace, "--quiet"],
346
+ { env: { LING_INDEX_PATH: indexPath } },
347
+ );
348
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
349
+ assert.ok(fs.existsSync(path.join(localWorkspace, ".agent")), "shared .agent should be created");
350
+
351
+ const indexData = JSON.parse(fs.readFileSync(indexPath, "utf8"));
352
+ const record = (indexData.workspaces || []).find((item) => item.path === localWorkspace);
353
+ assert.ok(record, "workspace should be indexed");
354
+ assert.ok(record.targets && record.targets.antigravity, "antigravity target should be indexed");
355
+ assert.ok(!record.targets.gemini, "gemini target should not be auto-indexed");
356
+ } finally {
357
+ fs.rmSync(localWorkspace, { recursive: true, force: true });
358
+ }
359
+ });
360
+
361
+ test("status should reflect antigravity target when workspace is registered as antigravity", () => {
362
+ const localWorkspace = fs.mkdtempSync(path.join(REPO_ROOT, ".tmp-ling-antigravity-status-"));
363
+ try {
364
+ const initResult = runCli(
365
+ ["init", "--target", "antigravity", "--path", localWorkspace, "--quiet"],
366
+ { env: { LING_INDEX_PATH: indexPath } },
367
+ );
368
+ assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
369
+
370
+ const result = runCli(
371
+ ["status", "--path", localWorkspace],
372
+ { env: { LING_INDEX_PATH: indexPath } },
373
+ );
374
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
375
+ assert.match(result.stdout || "", /Targets: antigravity/);
376
+ assert.match(result.stdout || "", /\[antigravity\]/);
377
+ assert.doesNotMatch(result.stdout || "", /\[gemini\]/);
378
+ } finally {
379
+ fs.rmSync(localWorkspace, { recursive: true, force: true });
380
+ }
381
+ });
382
+
383
+ test("antigravity status should remain correct without index via local install-state", () => {
384
+ const localWorkspace = fs.mkdtempSync(path.join(REPO_ROOT, ".tmp-ling-antigravity-no-index-"));
385
+ try {
386
+ const initResult = runCli(
387
+ ["init", "--target", "antigravity", "--path", localWorkspace, "--no-index", "--quiet"],
388
+ { env: { LING_INDEX_PATH: indexPath } },
389
+ );
390
+ assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
391
+
392
+ const installStatePath = path.join(localWorkspace, ".ling", "install-state.json");
393
+ assert.ok(fs.existsSync(installStatePath), "local install-state should be created");
394
+
395
+ const result = runCli(
396
+ ["status", "--path", localWorkspace],
397
+ { env: { LING_INDEX_PATH: indexPath } },
398
+ );
399
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
400
+ assert.match(result.stdout || "", /Targets: antigravity/);
401
+ assert.match(result.stdout || "", /\[antigravity\]/);
402
+
403
+ if (fs.existsSync(indexPath)) {
404
+ const indexData = JSON.parse(fs.readFileSync(indexPath, "utf8"));
405
+ const hasWorkspace = (indexData.workspaces || []).some((item) => item.path === localWorkspace);
406
+ assert.ok(!hasWorkspace, "workspace should still be absent from global index");
407
+ }
408
+ } finally {
409
+ fs.rmSync(localWorkspace, { recursive: true, force: true });
410
+ }
411
+ });
412
+
341
413
  test("update should create preflight backup for codex when unknown files exist in managed dir", () => {
342
414
  const initResult = runCli(
343
415
  ["init", "--target", "codex", "--path", workspaceDir, "--quiet"],
@@ -502,6 +574,49 @@ describe("CLI Smoke", () => {
502
574
  }
503
575
  });
504
576
 
577
+ test("update-all should allow explicit antigravity target on shared .agent workspace", () => {
578
+ const localWorkspace = fs.mkdtempSync(path.join(REPO_ROOT, ".tmp-ling-update-all-antigravity-"));
579
+ try {
580
+ const initResult = runCli(
581
+ ["init", "--target", "gemini", "--path", localWorkspace, "--quiet"],
582
+ { env: { LING_INDEX_PATH: indexPath } },
583
+ );
584
+ assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
585
+
586
+ const now = new Date().toISOString();
587
+ const seedIndex = {
588
+ version: 2,
589
+ updatedAt: now,
590
+ workspaces: [
591
+ {
592
+ path: localWorkspace,
593
+ targets: {
594
+ gemini: {
595
+ version: "2.0.1",
596
+ installedAt: now,
597
+ updatedAt: now,
598
+ },
599
+ },
600
+ },
601
+ ],
602
+ excludedPaths: [],
603
+ };
604
+ fs.writeFileSync(indexPath, `${JSON.stringify(seedIndex, null, 2)}\n`, "utf8");
605
+
606
+ const result = runCli(["update-all", "--targets", "antigravity", "--quiet"], {
607
+ env: { LING_INDEX_PATH: indexPath },
608
+ });
609
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
610
+
611
+ const indexData = JSON.parse(fs.readFileSync(indexPath, "utf8"));
612
+ const record = (indexData.workspaces || []).find((item) => item.path === localWorkspace);
613
+ assert.ok(record, "workspace should remain in index");
614
+ assert.ok(record.targets && record.targets.antigravity, "antigravity target should be added into index");
615
+ } finally {
616
+ fs.rmSync(localWorkspace, { recursive: true, force: true });
617
+ }
618
+ });
619
+
505
620
  test("init should respect --no-index on non-temp workspace", () => {
506
621
  const localWorkspace = fs.mkdtempSync(path.join(REPO_ROOT, ".tmp-ling-no-index-"));
507
622
  try {
@@ -63,7 +63,7 @@ describe("Global Sync", () => {
63
63
  assert.ok(fs.existsSync(path.join(skillsRoot, "workflow-plan", "SKILL.md")), "missing expected workflow skill: workflow-plan");
64
64
  });
65
65
 
66
- test("global sync should default to syncing codex and gemini when no target is provided", () => {
66
+ test("global sync should default to syncing codex, gemini, and antigravity when no target is provided", () => {
67
67
  const result = runCli(["global", "sync", "--quiet"], {
68
68
  env: { LING_GLOBAL_ROOT: tempDir },
69
69
  });
@@ -93,7 +93,7 @@ describe("Global Sync", () => {
93
93
  assert.strictEqual((statusResult.stdout || "").trim(), "installed");
94
94
  });
95
95
 
96
- test("global sync should install gemini skills into both ~/.gemini/skills and ~/.gemini/antigravity/skills", () => {
96
+ test("global sync should install gemini skills only into ~/.gemini/skills", () => {
97
97
  const result = runCli(["global", "sync", "--target", "gemini", "--quiet"], {
98
98
  env: { LING_GLOBAL_ROOT: tempDir },
99
99
  });
@@ -103,6 +103,19 @@ describe("Global Sync", () => {
103
103
  assert.ok(fs.existsSync(geminiCliRoot), "missing global gemini-cli skills root");
104
104
  assert.ok(fs.existsSync(path.join(geminiCliRoot, "clean-code", "SKILL.md")), "missing expected gemini-cli skill: clean-code");
105
105
 
106
+ const antigravityRoot = path.join(tempDir, ".gemini", "antigravity", "skills");
107
+ assert.ok(!fs.existsSync(antigravityRoot), "gemini sync should not create antigravity skills root");
108
+ });
109
+
110
+ test("global sync should install antigravity skills only into ~/.gemini/antigravity/skills", () => {
111
+ const result = runCli(["global", "sync", "--target", "antigravity", "--quiet"], {
112
+ env: { LING_GLOBAL_ROOT: tempDir },
113
+ });
114
+ assert.strictEqual(result.status, 0, result.stderr || result.stdout);
115
+
116
+ const geminiCliRoot = path.join(tempDir, ".gemini", "skills");
117
+ assert.ok(!fs.existsSync(geminiCliRoot), "antigravity sync should not create gemini-cli skills root");
118
+
106
119
  const antigravityRoot = path.join(tempDir, ".gemini", "antigravity", "skills");
107
120
  assert.ok(fs.existsSync(antigravityRoot), "missing global antigravity skills root");
108
121
  assert.ok(fs.existsSync(path.join(antigravityRoot, "clean-code", "SKILL.md")), "missing expected antigravity skill: clean-code");
@@ -62,6 +62,64 @@ describe("Spec init/doctor", () => {
62
62
  assert.strictEqual((doctorResult.stdout || "").trim(), "installed");
63
63
  });
64
64
 
65
+ test("spec init --csv-only should generate issues.csv and rely on global spec assets", () => {
66
+ const env = {
67
+ LING_GLOBAL_ROOT: tempRoot,
68
+ LING_INDEX_PATH: path.join(tempRoot, "workspaces.json"),
69
+ };
70
+
71
+ const enableResult = runCli(["spec", "enable", "--target", "codex", "--quiet"], { env });
72
+ assert.strictEqual(enableResult.status, 0, enableResult.stderr || enableResult.stdout);
73
+
74
+ const initResult = runCli(["spec", "init", "--path", workspaceRoot, "--csv-only", "--quiet"], { env });
75
+ assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
76
+
77
+ assert.ok(fs.existsSync(path.join(workspaceRoot, "issues.csv")), "issues.csv should be created");
78
+ assert.ok(fs.existsSync(path.join(workspaceRoot, "docs", "reviews")), "docs/reviews should exist");
79
+ assert.ok(fs.existsSync(path.join(workspaceRoot, "docs", "handoff")), "docs/handoff should exist");
80
+ assert.ok(!fs.existsSync(path.join(workspaceRoot, ".ling", "spec")), "spec assets should not be created in csv-only mode");
81
+
82
+ const doctorResult = runCli(["spec", "doctor", "--path", workspaceRoot, "--quiet"], { env });
83
+ assert.strictEqual(doctorResult.status, 0, doctorResult.stderr || doctorResult.stdout);
84
+ assert.strictEqual((doctorResult.stdout || "").trim(), "installed");
85
+ });
86
+
87
+ test("spec init without --path should initialize default spec-workspace and include targets", () => {
88
+ const nonTempRoot = fs.mkdtempSync(path.join(REPO_ROOT, ".tmp-ling-spec-global-"));
89
+ const indexPath = path.join(nonTempRoot, "workspaces.json");
90
+ const env = {
91
+ LING_GLOBAL_ROOT: nonTempRoot,
92
+ LING_INDEX_PATH: indexPath,
93
+ };
94
+
95
+ try {
96
+ const initResult = runCli(["spec", "init", "--spec-workspace", "--quiet"], { env });
97
+ assert.strictEqual(initResult.status, 0, initResult.stderr || initResult.stdout);
98
+
99
+ const defaultWorkspace = path.join(nonTempRoot, ".ling", "spec-workspace");
100
+ assert.ok(fs.existsSync(path.join(defaultWorkspace, "issues.csv")), "default workspace should include issues.csv");
101
+ assert.ok(
102
+ fs.existsSync(path.join(defaultWorkspace, ".ling", "spec", "templates", "driver-prompt.md")),
103
+ "default workspace should include spec templates",
104
+ );
105
+ assert.ok(fs.existsSync(path.join(defaultWorkspace, ".agent")), "default workspace should include gemini .agent");
106
+ assert.ok(fs.existsSync(path.join(defaultWorkspace, ".agents")), "default workspace should include codex .agents");
107
+
108
+ const doctorResult = runCli(["spec", "doctor", "--spec-workspace", "--quiet"], { env });
109
+ assert.strictEqual(doctorResult.status, 0, doctorResult.stderr || doctorResult.stdout);
110
+ assert.strictEqual((doctorResult.stdout || "").trim(), "installed");
111
+
112
+ assert.ok(fs.existsSync(indexPath), "workspace index should be created for non-temp global root");
113
+ const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
114
+ assert.ok(
115
+ (index.workspaces || []).some((workspace) => workspace && workspace.path === defaultWorkspace),
116
+ "default spec-workspace should be registered into index",
117
+ );
118
+ } finally {
119
+ fs.rmSync(nonTempRoot, { recursive: true, force: true });
120
+ }
121
+ });
122
+
65
123
  test("spec doctor should report broken when multiple tasks are in 进行中", () => {
66
124
  const env = {
67
125
  LING_GLOBAL_ROOT: tempRoot,
@@ -114,6 +114,22 @@ describe("Spec Profile", () => {
114
114
  assert.ok(fs.existsSync(path.join(codexSkillDir, "SKILL.md")), "spec skill should be repaired");
115
115
  });
116
116
 
117
+ test("spec enable should install antigravity skills independently", () => {
118
+ const env = { LING_GLOBAL_ROOT: tempRoot };
119
+
120
+ const enableResult = runCli(["spec", "enable", "--target", "antigravity", "--quiet"], { env });
121
+ assert.strictEqual(enableResult.status, 0, enableResult.stderr || enableResult.stdout);
122
+
123
+ const antigravitySkill = path.join(tempRoot, ".gemini", "antigravity", "skills", "harness-engineering", "SKILL.md");
124
+ const geminiSkill = path.join(tempRoot, ".gemini", "skills", "harness-engineering", "SKILL.md");
125
+ assert.ok(fs.existsSync(antigravitySkill), "missing installed antigravity spec skill");
126
+ assert.ok(!fs.existsSync(geminiSkill), "antigravity spec enable should not install gemini skill");
127
+
128
+ const statusResult = runCli(["spec", "status", "--quiet"], { env });
129
+ assert.strictEqual(statusResult.status, 0);
130
+ assert.strictEqual((statusResult.stdout || "").trim(), "installed");
131
+ });
132
+
117
133
  test("spec status should report broken when an asset file is missing", () => {
118
134
  const env = { LING_GLOBAL_ROOT: tempRoot };
119
135
 
@@ -144,6 +144,37 @@ describe('Standards Compliance', () => {
144
144
  assert.ok(!content.includes('$HOME/.agents/skills/'), 'should not contain deprecated global path: $HOME/.agents/skills/');
145
145
  });
146
146
 
147
+ test('README spec section should reflect implemented spec commands', () => {
148
+ const file = path.resolve('README.md');
149
+ const content = fs.readFileSync(file, 'utf8');
150
+
151
+ assert.ok(content.includes('ling spec init'), 'README should mention ling spec init');
152
+ assert.ok(content.includes('ling spec doctor'), 'README should mention ling spec doctor');
153
+ assert.ok(content.includes('~/.ling/spec/profiles/'), 'README should mention spec profiles path');
154
+ assert.ok(!content.includes('本版本尚未开放'), 'README should not claim spec project commands are unavailable');
155
+ assert.ok(!content.includes('spec init/remove/doctor'), 'README should not mention unsupported spec remove subcommand');
156
+ });
157
+
158
+ test('docs should describe gemini and antigravity as separate command targets', () => {
159
+ const readme = fs.readFileSync(path.resolve('README.md'), 'utf8');
160
+ const tech = fs.readFileSync(path.resolve('docs/TECH.md'), 'utf8');
161
+
162
+ assert.ok(readme.includes('ling init --target antigravity'), 'README should document project antigravity target');
163
+ assert.ok(readme.includes('ling global sync --target antigravity'), 'README should document global antigravity target');
164
+ assert.ok(tech.includes('`--target gemini` 只写入 gemini-cli;`--target antigravity` 只写入 antigravity'), 'TECH should describe split global targets');
165
+ assert.ok(!tech.includes('gemini 会同时写入 gemini-cli 与 antigravity'), 'TECH should not describe gemini as bundled antigravity target');
166
+ });
167
+
168
+ test('gitignore should whitelist curated reference materials only', () => {
169
+ const rootIgnore = fs.readFileSync(path.resolve('.gitignore'), 'utf8');
170
+ const referenceIgnore = fs.readFileSync(path.resolve('reference/.gitignore'), 'utf8');
171
+
172
+ assert.ok(rootIgnore.includes('!reference/official/**'), 'root .gitignore should keep reference/official tracked');
173
+ assert.ok(rootIgnore.includes('!reference/docs-archive/**'), 'root .gitignore should keep reference/docs-archive tracked');
174
+ assert.ok(referenceIgnore.includes('!official/**'), 'reference/.gitignore should keep official subtree tracked');
175
+ assert.ok(referenceIgnore.includes('!docs-archive/**'), 'reference/.gitignore should keep docs-archive subtree tracked');
176
+ });
177
+
147
178
  test('.agents script files should stay identical to reference snapshot', { skip: !HAS_REF_SCRIPTS_ROOT }, () => {
148
179
  const refScriptsRoot = REF_SCRIPTS_ROOT;
149
180
  const mismatches = [];