@fitlab-ai/agent-infra 0.8.3 → 0.8.5
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.zh-CN.md +10 -0
- package/bin/cli.ts +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/lib/cp.js +56 -15
- package/dist/lib/defaults.json +9 -0
- package/dist/lib/init.js +5 -0
- package/dist/lib/sandbox/capture.js +5 -3
- package/dist/lib/sandbox/clipboard/bridge.js +12 -9
- package/dist/lib/sandbox/clipboard/inbox.js +82 -0
- package/dist/lib/sandbox/commands/create.js +68 -26
- package/dist/lib/sandbox/engine.js +33 -3
- package/dist/lib/sandbox/engines/colima.js +2 -2
- package/dist/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/dist/lib/sandbox/shell.js +9 -1
- package/dist/lib/sandbox/tools.js +2 -2
- package/dist/lib/server/daemon.js +9 -0
- package/dist/lib/server/process-control.js +66 -73
- package/dist/lib/server/process-state.js +168 -0
- package/dist/lib/update.js +2 -0
- package/lib/cp.ts +56 -15
- package/lib/defaults.json +9 -0
- package/lib/init.ts +9 -0
- package/lib/sandbox/capture.ts +5 -3
- package/lib/sandbox/clipboard/bridge.ts +14 -12
- package/lib/sandbox/clipboard/inbox.ts +83 -0
- package/lib/sandbox/commands/create.ts +80 -27
- package/lib/sandbox/engine.ts +37 -3
- package/lib/sandbox/engines/colima.ts +2 -2
- package/lib/sandbox/runtimes/base.dockerfile +7 -0
- package/lib/sandbox/shell.ts +11 -2
- package/lib/sandbox/tools.ts +5 -5
- package/lib/server/daemon.ts +9 -0
- package/lib/server/process-control.ts +70 -71
- package/lib/server/process-state.ts +183 -0
- package/lib/update.ts +6 -0
- package/package.json +2 -1
- package/runtime/platform-adapters/platform-sync.github.js +1501 -0
- package/templates/.agents/README.en.md +5 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/rules/create-issue.github.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +5 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +5 -4
- package/templates/.agents/rules/testing-discipline.en.md +3 -41
- package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
- package/templates/.agents/scripts/lib/agent-infra-package.js +149 -0
- package/templates/.agents/scripts/lib/post-review-commit.js +5 -0
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +25 -1459
- package/templates/.agents/scripts/review-diff-fingerprint.js +95 -24
- package/templates/.agents/scripts/validate-artifact.js +144 -38
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +6 -4
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +35 -8
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +35 -8
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +27 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +27 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-milestones/SKILL.en.md +5 -4
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +6 -5
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +170 -45
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -2
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +203 -142
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -1
|
@@ -165,11 +165,14 @@ Recommended frontmatter:
|
|
|
165
165
|
name: enforce-style
|
|
166
166
|
description: "Apply the team style guide before code review. Use when you need to align the team's code style before review"
|
|
167
167
|
args: "<task-id>" # optional
|
|
168
|
+
disable-model-invocation: true # optional; consumed by supporting TUI adapters
|
|
168
169
|
---
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
Write `description` as "one-line responsibility + scenario clause": after the short responsibility, append "Use when …" (the Chinese `SKILL.zh-CN.md` variant uses "当……时使用") as the cross-TUI trigger semantics, so Agent Skills-capable tools can self-discover the skill in natural conversation. Do **not** add a separate `triggers` (or similar) frontmatter field for this.
|
|
172
173
|
|
|
174
|
+
Set the generic optional `disable-model-invocation` field to `true` when generated TUI commands should disable model invocation; each TUI adapter consumes it according to its capabilities. To preserve line breaks in a multiline `description`, use a YAML literal block (`|`); sync emits the corresponding multiline metadata format for each TUI.
|
|
175
|
+
|
|
173
176
|
After adding or updating a custom skill, run `update-agent-infra` again. The sync step detects non-built-in skills and generates matching commands for Claude Code, Gemini CLI, and OpenCode automatically.
|
|
174
177
|
|
|
175
178
|
### Shared skill sources
|
|
@@ -206,6 +209,8 @@ The `files` field in `.agents/.airc.json` groups project files into three catego
|
|
|
206
209
|
| `merged` | Merge semantically by AI or humans | Do not write from the template | Keep the local project copy |
|
|
207
210
|
| `ejected` | May be created from the template first; skip overwrite once it exists | Do not write from the template | Keep the local project copy |
|
|
208
211
|
|
|
212
|
+
Ordinary `managed` files keep their overwrite semantics. A small set of platform lifecycle workflows can be registered as guarded managed files. The synchronizer stores each rendered source SHA-256 in `files.managedBaselines` and uses a three-way comparison: accept template-only upgrades, preserve user-only changes, and report a conflict without overwriting when both sides changed or the origin is unknown. This mapping is tool-maintained and should not be edited manually.
|
|
213
|
+
|
|
209
214
|
`ejected` has two common uses:
|
|
210
215
|
|
|
211
216
|
1. **Taking over a built-in file**: the project needs full control over a rule, command, or config file that originally came from the template.
|
|
@@ -165,11 +165,14 @@
|
|
|
165
165
|
name: enforce-style
|
|
166
166
|
description: "在代码审查前应用团队风格规范。当需要在评审前统一团队代码风格时使用"
|
|
167
167
|
args: "<task-id>" # 可选
|
|
168
|
+
disable-model-invocation: true # 可选;由支持该能力的 TUI 适配器消费
|
|
168
169
|
---
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
`description` 采用「一句话职责 + 场景触发子句」写法:在简短职责描述后补充「当……时使用」(英文 `SKILL.en.md` 用「Use when …」),作为跨 TUI 的触发语义,供支持 Agent Skills 的工具在自然对话中自发现该 skill。**不要**为此新增 `triggers` 等额外 frontmatter 字段。
|
|
172
173
|
|
|
174
|
+
当生成的 TUI 命令需要禁用模型调用时,将通用可选字段 `disable-model-invocation` 设为 `true`;各 TUI 适配器按自身能力决定是否消费。多行 `description` 如需保留换行,使用 YAML literal block(`|`);同步器会为各 TUI 输出对应的多行元数据格式。
|
|
175
|
+
|
|
173
176
|
新增或修改自定义 skill 后,再执行一次 `update-agent-infra`。同步过程会自动检测非内置 skill,并为 Claude Code、Gemini CLI、OpenCode 生成对应命令。
|
|
174
177
|
|
|
175
178
|
### 共享 skill 源
|
|
@@ -206,6 +209,8 @@ args: "<task-id>" # 可选
|
|
|
206
209
|
| `merged` | 由 AI 或人工语义合并 | 不从模板写入 | 保留项目本地副本 |
|
|
207
210
|
| `ejected` | 首次可从模板创建,已存在时跳过覆盖 | 不从模板写入 | 保留项目本地副本 |
|
|
208
211
|
|
|
212
|
+
普通 `managed` 文件仍采用覆盖语义。少数平台生命周期 workflow 可登记为 guarded managed:同步器在 `files.managedBaselines` 保存渲染后来源的 SHA-256,并用三方比较自动接收官方单边升级、保留用户单边修改,在双方都变化或来源未知时报告冲突而不覆盖。该映射由工具维护,不建议手工编辑。
|
|
213
|
+
|
|
209
214
|
`ejected` 有两种常见用法:
|
|
210
215
|
|
|
211
216
|
1. **接管内置文件**:项目需要完全控制原本来自模板的规则、命令或配置文件,避免后续同步覆盖本地内容。
|
|
@@ -169,7 +169,7 @@ Field write failures are non-blocking.
|
|
|
169
169
|
Update task.md:
|
|
170
170
|
|
|
171
171
|
- Write `issue_number: {n}` into the frontmatter (replace if it exists; append at the end of the frontmatter otherwise)
|
|
172
|
-
- Update `updated_at` to the current time (command: `date "+%Y-%m-%d %H:%M:%S
|
|
172
|
+
- Update `updated_at` to the current time (command: `date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'`)
|
|
173
173
|
|
|
174
174
|
> Do NOT append an Activity Log entry here. The Issue creation event is already captured by the GitHub Issue itself and by the frontmatter `issue_number` field; the Activity Log only records the single `create-task` skill execution anchor (`Create Task`), written by the caller SKILL step 3.
|
|
175
175
|
|
|
@@ -169,7 +169,7 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH \
|
|
|
169
169
|
更新 task.md:
|
|
170
170
|
|
|
171
171
|
- 把 `issue_number: {n}` 写入 frontmatter(已存在则替换;不存在则在 frontmatter 末尾追加)
|
|
172
|
-
- 更新 `updated_at` 为当前时间(命令:`date "+%Y-%m-%d %H:%M:%S
|
|
172
|
+
- 更新 `updated_at` 为当前时间(命令:`date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'`)
|
|
173
173
|
|
|
174
174
|
> 不要在此追加 Activity Log 条目。Issue 创建事件已由 GitHub Issue 自身和 frontmatter `issue_number` 承载;Activity Log 仅记录 `create-task` skill 一次执行的整体锚点(`Create Task`),由调用方 SKILL 步骤 3 写入。
|
|
175
175
|
|
|
@@ -85,10 +85,11 @@ When an executor judges an item to be a key design decision that needs human rul
|
|
|
85
85
|
|
|
86
86
|
## post-review commit gate (code stage only)
|
|
87
87
|
|
|
88
|
-
-
|
|
89
|
-
- `commit` reads only the highest-round `review-code` artifact.
|
|
90
|
-
-
|
|
91
|
-
-
|
|
88
|
+
- `review-code` captures the review baseline `R` (diff base), full-worktree diff fingerprint `F`, and normalized snapshot tree `T`. An Approved clean snapshot may set `B=R`; an Approved snapshot with uncommitted changes clears or omits `B`.
|
|
89
|
+
- `commit` reads only the highest-round Approved `review-code` artifact. Before committing it requires `pre_head == R`, complete worktree tree `W == T`, and normalized staged tree `S == T`; any mismatch blocks before `git commit` and reports added, missing, and different paths for both comparisons.
|
|
90
|
+
- After a successful commit it sets `B=last_reviewed_commit=<new_head>`; B means only a reviewed snapshot anchored to a Git commit.
|
|
91
|
+
- The `complete-task` `post-review-commit` gate uses only B. When B is absent, malformed, or missing as a Git object, it reports `reviewed snapshot was not anchored` and never falls back to R.
|
|
92
|
+
- If new commits touch code / rule paths after B, the gate blocks and requires a fresh `review-code`.
|
|
92
93
|
- **Exemption**: append a ledger row `| PRC-1 | post-review-commit | - | - | human-decided | <ruling note> |` recording that a human explicitly allowed those commits without re-review.
|
|
93
94
|
|
|
94
95
|
## Gate behavior cheat sheet
|
|
@@ -85,10 +85,11 @@
|
|
|
85
85
|
|
|
86
86
|
## post-review commit 门禁(仅 code 阶段)
|
|
87
87
|
|
|
88
|
-
- `review-code`
|
|
89
|
-
- `commit` 只读取最高轮 `review-code`
|
|
90
|
-
-
|
|
91
|
-
-
|
|
88
|
+
- `review-code` 一次性捕获审查基线 `R`(diff base)、完整工作区差异指纹 `F` 与规范化快照树 `T`;Approved 且快照干净时可令 `B=R`,Approved 且含未提交差异时清除/不写 `B`。
|
|
89
|
+
- `commit` 只读取最高轮 Approved `review-code` 产物;提交前比较 `pre_head == R`、完整工作区树 `W == T`、规范化暂存树 `S == T`,任一失配都在 `git commit` 前阻断并报告两组 added/missing/different 路径。
|
|
90
|
+
- 成功提交后令 `B=last_reviewed_commit=<new_head>`;`B` 只表示已经落到 Git commit 的审查锚点。
|
|
91
|
+
- `complete-task` 的 `post-review-commit` gate 只使用 B;B 缺失、畸形或对象不存在时报告 `reviewed snapshot was not anchored`,不得回退 R。
|
|
92
|
+
- 若 B 之后代码 / 规则路径出现新提交,gate 会拦截,要求重新 `review-code`。
|
|
92
93
|
- **豁免**:在账本追加一行 `| PRC-1 | post-review-commit | - | - | human-decided | <裁定说明> |`,记录人工明确允许该批提交免复审。
|
|
93
94
|
|
|
94
95
|
## gate 行为速查
|
|
@@ -39,46 +39,8 @@ This mirrors "Goal-Driven Execution" in AGENTS.md: define a verifiable success c
|
|
|
39
39
|
- **Testing implementation details**: Prefer assertions on public APIs, artifacts, state changes, or error results; avoid assertions on private functions, internal call order, or temporary data structures.
|
|
40
40
|
- **Insufficient assertions**: Assertions must pin down concrete expected values; do not replace checks on key fields, counts, and boundaries with "does not throw" or "result exists".
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Project-specific test policy
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Keep repository-specific commands, directory conventions, coverage thresholds, CI integration, and reporting services in the project's own test documentation. Before adding a test, inspect that local policy and place the test in the layer that matches its observable scope and runtime cost.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm run test:coverage
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The tail of stdout prints per-file line / branch / function coverage and the uncovered line numbers.
|
|
53
|
-
|
|
54
|
-
### CI display
|
|
55
|
-
|
|
56
|
-
`.github/workflows/unit-tests.yml` writes the coverage block into the GitHub Actions step summary on the ubuntu-latest shard (visible on the PR Checks page). Windows / macOS shards do not duplicate this output.
|
|
57
|
-
|
|
58
|
-
The Codecov badge at the top of the README is generated by Codecov after `.github/workflows/unit-tests.yml` uploads `coverage.lcov` from the ubuntu-latest shard.
|
|
59
|
-
|
|
60
|
-
### Boundaries
|
|
61
|
-
|
|
62
|
-
- **No percentage thresholds**: Threshold flags such as `--test-coverage-lines/branches/functions` are forbidden. Goodhart's law reminds us that once coverage becomes a metric, developers write "coverage-friendly but behavior-weak" tests.
|
|
63
|
-
- **Third-party services for the badge only**: Codecov hosts the README coverage badge, but the root `codecov.yml` explicitly disables its project/patch status checks and PR comments — Codecov only displays a number here and does not participate in merge decisions. Do not integrate other services such as coveralls.
|
|
64
|
-
- **No tier split**: Coverage is currently emitted only for the full `test` tier; smoke / core tier coverage has no independent value.
|
|
65
|
-
- **Does not block PRs**: The CI step uses `continue-on-error: true`, so a failed coverage collection does not affect merges.
|
|
66
|
-
|
|
67
|
-
### Where New Tests Go
|
|
68
|
-
|
|
69
|
-
The test directory determines which npm script runs a test file:
|
|
70
|
-
|
|
71
|
-
- `tests/unit/<module>/`: fast structural or pure-function tests; does not spawn the real CLI process or depend on external tools, suitable for `test:smoke`.
|
|
72
|
-
- `tests/integration/<module>/`: combines multiple modules, runs CLI subprocesses, touches temporary filesystems, or verifies template sync flows while staying stable and reasonably fast, suitable for `test:core`.
|
|
73
|
-
- `tests/e2e/<module>/`: slower contract, platform-sync, packaged-output, cross-process, or end-to-end workflow tests, run only by full `npm test`.
|
|
74
|
-
|
|
75
|
-
Modules remain as second-level directories such as `cli`, `core`, `scripts`, and `templates`. Shared helpers and fixtures stay in `tests/helpers/`, `tests/helpers.ts`, and `tests/fixtures/`; do not place them under a tier.
|
|
76
|
-
|
|
77
|
-
### Relation to "test tier coverage"
|
|
78
|
-
|
|
79
|
-
Distinguish two concepts:
|
|
80
|
-
|
|
81
|
-
- **Test tier coverage** (`tests/unit/core/test-tier-coverage.test.ts` checks test directory placement and npm script tier mapping): governs "which test files belong to which tier" and is orthogonal to source-line coverage.
|
|
82
|
-
- **Source-line coverage** (this section): governs "which lines of production source are exercised by tests".
|
|
83
|
-
|
|
84
|
-
The two serve different purposes and should not substitute for each other.
|
|
46
|
+
If the project does not define a layered test suite, use its complete test command for RED and GREEN verification. Do not invent a test hierarchy or coverage gate as part of an unrelated change.
|
|
@@ -39,46 +39,8 @@ assert.match(content, /^name: code-task$/m); // 正向断言已足够
|
|
|
39
39
|
- **测试实现细节**:优先断言公开接口、产物、状态变化或错误结果;避免断言私有函数、内部调用顺序、临时数据结构。
|
|
40
40
|
- **断言不充分**:断言必须锁定具体期望值;不要用"只要不抛异常""结果存在即可"替代对关键字段、数量和边界的验证。
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## 项目级测试策略
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
仓库特定的命令、目录约定、覆盖率阈值、CI 集成和报告服务应记录在项目自己的测试文档中。新增测试前先读取该策略,并按测试的可观察范围与运行成本选择对应层级。
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm run test:coverage
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
stdout 末尾会打印按文件粒度的行 / 分支 / 函数覆盖率以及未覆盖行号。
|
|
53
|
-
|
|
54
|
-
### CI 展示
|
|
55
|
-
|
|
56
|
-
`.github/workflows/unit-tests.yml` 在 ubuntu-latest 分片上把覆盖率块写入 GitHub Actions 的 step summary(PR Checks 页可见)。Windows / macOS 分片不重复输出。
|
|
57
|
-
|
|
58
|
-
README 顶部的 Codecov 徽章由 `.github/workflows/unit-tests.yml` 在 ubuntu-latest 分片上传 `coverage.lcov` 后由 Codecov 生成。
|
|
59
|
-
|
|
60
|
-
### 边界
|
|
61
|
-
|
|
62
|
-
- **不设置百分比阈值**:`--test-coverage-lines/branches/functions` 等阈值参数禁止加入;Goodhart's law 提醒我们一旦把覆盖率作为指标,开发者会写"覆盖率友好但行为弱"的测试。
|
|
63
|
-
- **第三方服务仅用于徽章**:已接入 Codecov 托管 README 覆盖率徽章,但通过根 `codecov.yml` 显式关闭其 project/patch status check 与 PR 评论——Codecov 在本项目只展示数字,不参与 merge 决策。不接入 coveralls 等其他服务。
|
|
64
|
-
- **不区分 tier**:当前只对 full `test` tier 输出覆盖率;smoke / core tier 的覆盖率没有独立价值。
|
|
65
|
-
- **不阻塞 PR**:CI 步骤 `continue-on-error: true`,即便覆盖率采集失败也不影响 merge。
|
|
66
|
-
|
|
67
|
-
### 新测试该放哪一层
|
|
68
|
-
|
|
69
|
-
测试文件放入哪一层决定它会被哪些 npm script 自动执行:
|
|
70
|
-
|
|
71
|
-
- `tests/unit/<module>/`:快速、结构性或纯函数类测试;不启动真实 CLI 子进程,不依赖外部工具,适合 `test:smoke`。
|
|
72
|
-
- `tests/integration/<module>/`:会组合多个模块、运行 CLI 子进程、触达临时文件系统或验证模板同步流程,但仍应保持稳定和相对快速,适合 `test:core`。
|
|
73
|
-
- `tests/e2e/<module>/`:较慢的契约、平台同步、打包产物、跨进程或端到端流程测试,只在完整 `npm test` 中运行。
|
|
74
|
-
|
|
75
|
-
模块继续作为第二级目录(如 `cli`、`core`、`scripts`、`templates`)。共享 helper 和 fixtures 保持在 `tests/helpers/`、`tests/helpers.ts`、`tests/fixtures/`,不要放入任一 tier。
|
|
76
|
-
|
|
77
|
-
### 与"测试 tier 覆盖"的关系
|
|
78
|
-
|
|
79
|
-
注意区分两个概念:
|
|
80
|
-
|
|
81
|
-
- **测试 tier 覆盖**(`tests/unit/core/test-tier-coverage.test.ts` 校验测试文件目录归属与 npm script tier 映射):管的是"哪些测试文件被纳入哪一 tier",与代码行覆盖率正交。
|
|
82
|
-
- **代码行覆盖率**(本节):管的是"业务源码哪些行被测试触达"。
|
|
83
|
-
|
|
84
|
-
两者目的不同,不要相互替代。
|
|
46
|
+
如果项目没有定义分层测试套件,RED 与 GREEN 验证都使用项目的完整测试命令。不要在无关改动中自行引入测试层级或覆盖率门禁。
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const PACKAGE_NAME = "@fitlab-ai/agent-infra";
|
|
7
|
+
const RUNTIME_RELATIVE_PATH = "runtime/platform-adapters/platform-sync.github.js";
|
|
8
|
+
const COMMAND_NAMES = ["ai", "agent-infra"];
|
|
9
|
+
|
|
10
|
+
function canonicalize(candidate) {
|
|
11
|
+
try {
|
|
12
|
+
return fs.realpathSync(candidate);
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function inspectPackageRoot(candidate, source) {
|
|
19
|
+
const canonicalRoot = canonicalize(candidate);
|
|
20
|
+
if (!canonicalRoot) {
|
|
21
|
+
return { source, candidate, reason: "path does not exist" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const packagePath = path.join(canonicalRoot, "package.json");
|
|
25
|
+
let metadata;
|
|
26
|
+
try {
|
|
27
|
+
metadata = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return { source, candidate: canonicalRoot, reason: `cannot read ${packagePath}` };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (metadata.name !== PACKAGE_NAME) {
|
|
33
|
+
return {
|
|
34
|
+
source,
|
|
35
|
+
candidate: canonicalRoot,
|
|
36
|
+
reason: `${packagePath} does not belong to ${PACKAGE_NAME}`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const runtimePath = path.join(canonicalRoot, RUNTIME_RELATIVE_PATH);
|
|
41
|
+
if (!fs.existsSync(runtimePath)) {
|
|
42
|
+
return { source, candidate: canonicalRoot, reason: `runtime not found at ${runtimePath}` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
source,
|
|
47
|
+
candidate: canonicalRoot,
|
|
48
|
+
packageRoot: canonicalRoot,
|
|
49
|
+
runtimePath,
|
|
50
|
+
templateRoot: path.join(canonicalRoot, "templates")
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function packageRootsAbove(startPath) {
|
|
55
|
+
const roots = [];
|
|
56
|
+
let current = path.dirname(startPath);
|
|
57
|
+
while (true) {
|
|
58
|
+
if (fs.existsSync(path.join(current, "package.json"))) roots.push(current);
|
|
59
|
+
const parent = path.dirname(current);
|
|
60
|
+
if (parent === current) break;
|
|
61
|
+
current = parent;
|
|
62
|
+
}
|
|
63
|
+
return roots;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function lookupCommand(command, platform) {
|
|
67
|
+
const lookup = platform === "win32" ? `where ${command}` : `command -v ${command}`;
|
|
68
|
+
try {
|
|
69
|
+
return childProcess.execSync(lookup, {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
72
|
+
}).trim().split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function rootsForCommand(commandPath, platform) {
|
|
79
|
+
const roots = [];
|
|
80
|
+
if (platform === "win32" && /\.cmd$/i.test(commandPath)) {
|
|
81
|
+
roots.push(path.join(path.dirname(commandPath), "node_modules", "@fitlab-ai", "agent-infra"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const canonicalCommand = canonicalize(commandPath);
|
|
85
|
+
if (canonicalCommand) roots.push(...packageRootsAbove(canonicalCommand));
|
|
86
|
+
return roots;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveAgentInfraPackage(options = {}) {
|
|
90
|
+
const env = options.env || process.env;
|
|
91
|
+
const platform = options.platform || process.platform;
|
|
92
|
+
const startPath = options.startPath || fileURLToPath(import.meta.url);
|
|
93
|
+
const attempts = [];
|
|
94
|
+
|
|
95
|
+
const tryRoot = (candidate, source) => {
|
|
96
|
+
const inspected = inspectPackageRoot(candidate, source);
|
|
97
|
+
attempts.push(inspected);
|
|
98
|
+
return inspected.packageRoot ? inspected : null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (env.AGENT_INFRA_PACKAGE_ROOT) {
|
|
102
|
+
const explicit = tryRoot(env.AGENT_INFRA_PACKAGE_ROOT, "AGENT_INFRA_PACKAGE_ROOT");
|
|
103
|
+
if (explicit) return { ...explicit, attempts };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const candidate of packageRootsAbove(startPath)) {
|
|
107
|
+
const self = tryRoot(candidate, "current installation");
|
|
108
|
+
if (self) return { ...self, attempts };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const command of COMMAND_NAMES) {
|
|
112
|
+
const commandPaths = lookupCommand(command, platform);
|
|
113
|
+
if (commandPaths.length === 0) {
|
|
114
|
+
attempts.push({ source: `PATH:${command}`, candidate: null, reason: "command not found" });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
for (const commandPath of commandPaths) {
|
|
118
|
+
const roots = rootsForCommand(commandPath, platform);
|
|
119
|
+
if (roots.length === 0) {
|
|
120
|
+
attempts.push({ source: `PATH:${command}`, candidate: commandPath, reason: "package root not found" });
|
|
121
|
+
}
|
|
122
|
+
for (const candidate of roots) {
|
|
123
|
+
const resolved = tryRoot(candidate, `PATH:${command} (${commandPath})`);
|
|
124
|
+
if (resolved) return { ...resolved, attempts };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { packageRoot: null, runtimePath: null, templateRoot: null, attempts };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatAgentInfraPackageError(result) {
|
|
133
|
+
const details = result.attempts.length > 0
|
|
134
|
+
? result.attempts.map(({ source, candidate, reason }) =>
|
|
135
|
+
` - ${source}${candidate ? `: ${candidate}` : ""}: ${reason}`
|
|
136
|
+
).join("\n")
|
|
137
|
+
: " - no package candidates were found";
|
|
138
|
+
return [
|
|
139
|
+
"Unable to locate a trusted @fitlab-ai/agent-infra installation.",
|
|
140
|
+
"Attempted package locations:",
|
|
141
|
+
details,
|
|
142
|
+
"Install agent-infra persistently and ensure ai or agent-infra is on PATH:",
|
|
143
|
+
" npm install -g @fitlab-ai/agent-infra",
|
|
144
|
+
" brew install fitlab-ai/tap/agent-infra",
|
|
145
|
+
"A one-time npx invocation is only suitable for bootstrap; install before update or validation."
|
|
146
|
+
].join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { formatAgentInfraPackageError, resolveAgentInfraPackage };
|
|
@@ -47,6 +47,11 @@ export function extractReviewDiffFingerprint(content) {
|
|
|
47
47
|
return match ? match[1].trim().replace(/`/g, "") : "";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function extractReviewedSnapshotTree(content) {
|
|
51
|
+
const match = String(content).match(/^[-*]?\s*\*\*(?:审查快照树|Reviewed Snapshot Tree)\*\*[::]\s*(.*?)\s*$/m);
|
|
52
|
+
return match ? match[1].trim().replace(/`/g, "") : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
export function parseReviewVerdict(content) {
|
|
51
56
|
const match = String(content).match(/^[-*]?\s*\*\*(?:总体结论|Overall Verdict)\*\*[::]\s*(.*?)\s*$/m);
|
|
52
57
|
return match ? match[1].trim() : "";
|