@fitlab-ai/agent-infra 0.8.4 → 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/dist/lib/defaults.json +8 -0
- package/dist/lib/init.js +5 -0
- package/dist/lib/sandbox/commands/create.js +30 -4
- package/dist/lib/sandbox/engine.js +33 -3
- package/dist/lib/sandbox/engines/colima.js +2 -2
- package/dist/lib/sandbox/runtimes/base.dockerfile +4 -0
- 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/defaults.json +8 -0
- package/lib/init.ts +9 -0
- package/lib/sandbox/commands/create.ts +32 -3
- package/lib/sandbox/engine.ts +37 -3
- package/lib/sandbox/engines/colima.ts +2 -2
- package/lib/sandbox/runtimes/base.dockerfile +4 -0
- 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 +2 -0
- package/templates/.agents/README.zh-CN.md +2 -0
- package/templates/.agents/rules/review-handshake.en.md +5 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +5 -4
- 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 +68 -55
- package/templates/.agents/skills/commit/SKILL.en.md +5 -3
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +29 -2
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +29 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- 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/init-milestones/SKILL.en.md +2 -2
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +5 -4
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +2 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +2 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -1
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +174 -136
|
@@ -76,7 +76,7 @@ tail .agents/workspace/active/{task-id}/task.md
|
|
|
76
76
|
- 一次性记录 `R=$(git rev-parse HEAD)`;本轮报告、指纹和任务审查事实都复用该 R,禁止稍后重新读取 HEAD 代替
|
|
77
77
|
- `git diff --binary "$R" -- <post-review-globs>` 覆盖已跟踪变更
|
|
78
78
|
- `git ls-files -o --exclude-standard -z -- <post-review-globs>` 覆盖未跟踪新文件
|
|
79
|
-
- `node .agents/scripts/review-diff-fingerprint.js worktree "$R"`
|
|
79
|
+
- `node .agents/scripts/review-diff-fingerprint.js worktree "$R" --format json` 一次生成审查差异指纹 `F` 与审查快照树 `T`,并写入报告
|
|
80
80
|
|
|
81
81
|
> 详细审查标准、严重程度划分和 reviewer 关注点见 `reference/review-criteria.md`。执行此步骤前先读取 `reference/review-criteria.md`。
|
|
82
82
|
> 测试审查硬门禁:当 `git diff` 触及测试文件时,必须先读取 `.agents/rules/testing-discipline.md` 并逐条核对(尤其"正向已覆盖时不应再加反向断言")。
|
|
@@ -99,7 +99,7 @@ date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'
|
|
|
99
99
|
- `assigned_to`:{当前代理}
|
|
100
100
|
- `updated_at`:{当前时间}
|
|
101
101
|
- `agent_infra_version`:按 `.agents/rules/version-stamp.md` 取值
|
|
102
|
-
- 若本轮 `总体结论` / `Overall Verdict` 为 `通过` / `Approved`,写入 `last_reviewed_commit: {R}
|
|
102
|
+
- 若本轮 `总体结论` / `Overall Verdict` 为 `通过` / `Approved` 且 `T == R^{tree}`,写入 `last_reviewed_commit: {R}`;若 Approved 快照包含未提交差异,则清除既有 `last_reviewed_commit`,等待 `commit` 锚定
|
|
103
103
|
- 若本轮结论不是 Approved,保留既有 `last_reviewed_commit`,不得推进或清空
|
|
104
104
|
- 追加:
|
|
105
105
|
`- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Review Code (Round {N})** by {agent} — Verdict: {Approved/Changes Requested/Rejected}, blockers: {n}, major: {n}, minor: {n}, Manual-validation: {n} → {artifact-filename}`
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"^\\$ ",
|
|
33
33
|
"^- \\*\\*Overall Verdict\\*\\*[::]\\s*(?:通过|需要修改|拒绝|Approved|Changes Requested|Rejected)\\s*$",
|
|
34
34
|
"^- \\*\\*Review Baseline Commit\\*\\*[::]\\s*\\S",
|
|
35
|
-
"^- \\*\\*Reviewed Diff Fingerprint\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$"
|
|
35
|
+
"^- \\*\\*Reviewed Diff Fingerprint\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$",
|
|
36
|
+
"^- \\*\\*Reviewed Snapshot Tree\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$"
|
|
36
37
|
],
|
|
37
38
|
"freshness_minutes": 30
|
|
38
39
|
},
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"^\\$ ",
|
|
33
33
|
"^- \\*\\*总体结论\\*\\*[::]\\s*(?:通过|需要修改|拒绝|Approved|Changes Requested|Rejected)\\s*$",
|
|
34
34
|
"^- \\*\\*审查基线提交\\*\\*[::]\\s*\\S",
|
|
35
|
-
"^- \\*\\*审查差异指纹\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$"
|
|
35
|
+
"^- \\*\\*审查差异指纹\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$",
|
|
36
|
+
"^- \\*\\*审查快照树\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$"
|
|
36
37
|
],
|
|
37
38
|
"freshness_minutes": 30
|
|
38
39
|
},
|
|
@@ -21,8 +21,9 @@ Use this template when writing `review-code.md` or `review-code-r{N}.md`.
|
|
|
21
21
|
- **Reviewer**: {reviewer-name}
|
|
22
22
|
- **Review Time**: {timestamp}
|
|
23
23
|
- **Scope**: {file-count and major modules}
|
|
24
|
-
- **Review Baseline Commit**: {raw R captured once for this round} (
|
|
24
|
+
- **Review Baseline Commit**: {raw R captured once for this round} (diff base only; see `.agents/rules/review-handshake.md`)
|
|
25
25
|
- **Reviewed Diff Fingerprint**: {raw node .agents/scripts/review-diff-fingerprint.js worktree "$R"}
|
|
26
|
+
- **Reviewed Snapshot Tree**: {tree field from node .agents/scripts/review-diff-fingerprint.js worktree "$R" --format json}
|
|
26
27
|
- **Overall Verdict**: {Approved / Changes Requested / Rejected} (pick exactly one; combined phrases will fail the verify gate)
|
|
27
28
|
- **Findings (AI-actionable)**: 0 blockers, 0 majors, 0 minors / **Manual validation**: 0
|
|
28
29
|
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
- **审查者**:{reviewer-name}
|
|
22
22
|
- **审查时间**:{timestamp}
|
|
23
23
|
- **审查范围**:{file-count and major modules}
|
|
24
|
-
- **审查基线提交**:{本轮一次性捕获的 R 原文}
|
|
24
|
+
- **审查基线提交**:{本轮一次性捕获的 R 原文}(仅表示 diff base;详见 `.agents/rules/review-handshake.md`)
|
|
25
25
|
- **审查差异指纹**:{node .agents/scripts/review-diff-fingerprint.js worktree "$R" 原文}
|
|
26
|
+
- **审查快照树**:{node .agents/scripts/review-diff-fingerprint.js worktree "$R" --format json 的 tree 字段}
|
|
26
27
|
- **总体结论**:{通过 / 需要修改 / 拒绝}(恰取一个;禁止写组合短语,否则 verify gate 失败)
|
|
27
28
|
- **发现(AI 可处理)**:0 阻塞项,0 主要,0 次要 / **人工校验**:0
|
|
28
29
|
|
|
@@ -33,7 +33,7 @@ same-path external files, and later external sources win over earlier external
|
|
|
33
33
|
sources. Conflicts are recorded in the report. The script then performs:
|
|
34
34
|
- detect the template source version
|
|
35
35
|
- File registry sync (`defaults.json` → `.agents/.airc.json`)
|
|
36
|
-
- All managed files (language selection → exclude merged/ejected → placeholder rendering →
|
|
36
|
+
- All managed files (language selection → exclude merged/ejected → placeholder rendering → write; built-in guarded managed files use a source-baseline three-way comparison)
|
|
37
37
|
- Ejected files (create only on first install)
|
|
38
38
|
- `.agents/.airc.json` update (`templateVersion`, file lists)
|
|
39
39
|
|
|
@@ -47,6 +47,8 @@ The script outputs JSON to stdout. Parse and record the report.
|
|
|
47
47
|
list these in the report so users know which files were ignored because a
|
|
48
48
|
built-in template or later external source won
|
|
49
49
|
- `managed.written` / `managed.created`: updated / newly created managed files
|
|
50
|
+
- `managed.protected`: guarded managed files whose local user-only modification or deletion was preserved
|
|
51
|
+
- `managed.conflicts`: guarded managed files with unknown origin, two-sided changes, or ownership that cannot be proven during a platform switch; list each target, reason, and three-way hash
|
|
50
52
|
- `managed.removed`: deleted managed files (including old paths removed during template migrations)
|
|
51
53
|
- `managed.skippedPlatform`: managed / merged entries skipped because they belong to a different platform
|
|
52
54
|
- `managed.skippedTUI`: managed / merged entries skipped because they are owned by a built-in TUI that is disabled in `tuis` (customTUI command files under the same path prefix are preserved)
|
|
@@ -56,6 +58,8 @@ The script outputs JSON to stdout. Parse and record the report.
|
|
|
56
58
|
- `selfUpdate`: whether this is a self-update scenario
|
|
57
59
|
- `configUpdated`: whether `.agents/.airc.json` was updated
|
|
58
60
|
|
|
61
|
+
If `managed.conflicts` is non-empty, output every conflict and stop immediately before Phase B. Do not overwrite a conflicting file or advance its `files.managedBaselines` entry.
|
|
62
|
+
|
|
59
63
|
## Phase B: Process merged files (AI intelligent merge)
|
|
60
64
|
|
|
61
65
|
Process each item in the report's `merged.pending` list:
|
|
@@ -27,7 +27,7 @@ node .agents/skills/update-agent-infra/scripts/sync-templates.js
|
|
|
27
27
|
脚本读取 `.agents/.airc.json`,并通过 npm 自动定位已安装的 `@fitlab-ai/agent-infra/templates/` 目录。同步前会合并 `templates.sources` 中配置的外部模板源;相同文件以内置模板为准,多个外部源之间后者覆盖前者,冲突会写入报告。然后自动完成:
|
|
28
28
|
- 检测模板源版本
|
|
29
29
|
- 同步文件注册表(`defaults.json` → `.agents/.airc.json`)
|
|
30
|
-
- 处理所有 managed 文件(语言选择 → 排除 merged/ejected → 占位符渲染 →
|
|
30
|
+
- 处理所有 managed 文件(语言选择 → 排除 merged/ejected → 占位符渲染 → 写入;内建 guarded managed 文件使用来源基线三方比较)
|
|
31
31
|
- 处理 ejected 文件(仅首次安装时创建)
|
|
32
32
|
- 更新 `.agents/.airc.json`(`templateVersion`、文件列表)
|
|
33
33
|
|
|
@@ -39,6 +39,8 @@ node .agents/skills/update-agent-infra/scripts/sync-templates.js
|
|
|
39
39
|
- `templateRoot`:模板文件根目录绝对路径
|
|
40
40
|
- `templateSources.conflicts`:外部模板源冲突列表;报告中必须显式展示,说明哪些文件因内置模板或后续外部源获胜而被忽略
|
|
41
41
|
- `managed.written` / `managed.created`:已更新/新建的 managed 文件
|
|
42
|
+
- `managed.protected`:检测到用户单边修改或删除、因此保留本地状态的 guarded managed 文件
|
|
43
|
+
- `managed.conflicts`:来源未知、双边修改或平台切换时无法证明官方所有权的 guarded managed 冲突;必须逐项展示 target、reason 与三方哈希
|
|
42
44
|
- `managed.removed`:被删除的 managed 文件(包括模板迁移时移除的旧路径)
|
|
43
45
|
- `managed.skippedPlatform`:因归属其他平台而被跳过的 managed / merged 条目
|
|
44
46
|
- `managed.skippedTUI`:因 `tuis` 中未启用对应内建 TUI 而被跳过的 managed / merged 条目(落在同一路径前缀下的 customTUI 命令文件会被保留)
|
|
@@ -48,6 +50,8 @@ node .agents/skills/update-agent-infra/scripts/sync-templates.js
|
|
|
48
50
|
- `selfUpdate`:是否为自更新模式
|
|
49
51
|
- `configUpdated`:`.agents/.airc.json` 是否已更新
|
|
50
52
|
|
|
53
|
+
如果 `managed.conflicts` 非空,输出全部冲突并立即停止,不进入阶段 B;禁止覆盖冲突文件或推进其 `files.managedBaselines`。
|
|
54
|
+
|
|
51
55
|
## 阶段 B:处理 merged 文件(AI 智能合并)
|
|
52
56
|
|
|
53
57
|
根据报告中 `merged.pending` 列表,逐个文件处理。对于每个条目:
|
|
@@ -14,11 +14,17 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import childProcess from 'node:child_process';
|
|
17
|
+
import crypto from 'node:crypto';
|
|
17
18
|
import fs from 'node:fs';
|
|
18
19
|
import os from 'node:os';
|
|
19
20
|
import path from 'node:path';
|
|
20
21
|
import { fileURLToPath } from 'node:url';
|
|
21
22
|
|
|
23
|
+
import {
|
|
24
|
+
formatAgentInfraPackageError,
|
|
25
|
+
resolveAgentInfraPackage
|
|
26
|
+
} from '../../../scripts/lib/agent-infra-package.js';
|
|
27
|
+
|
|
22
28
|
const DEFAULTS = {
|
|
23
29
|
"platform": {
|
|
24
30
|
"type": "github"
|
|
@@ -65,8 +71,16 @@ const DEFAULTS = {
|
|
|
65
71
|
".gemini/commands/",
|
|
66
72
|
".git-hooks/check-version-format.sh",
|
|
67
73
|
".github/scripts/",
|
|
74
|
+
".github/workflows/metadata-sync.yml",
|
|
75
|
+
".github/workflows/pr-label.yml",
|
|
76
|
+
".github/workflows/status-label.yml",
|
|
68
77
|
".opencode/commands/"
|
|
69
78
|
],
|
|
79
|
+
"guardedManaged": [
|
|
80
|
+
".github/workflows/metadata-sync.yml",
|
|
81
|
+
".github/workflows/pr-label.yml",
|
|
82
|
+
".github/workflows/status-label.yml"
|
|
83
|
+
],
|
|
70
84
|
"merged": [
|
|
71
85
|
"**/post-release.*",
|
|
72
86
|
"**/release.*",
|
|
@@ -88,7 +102,6 @@ const DEFAULTS = {
|
|
|
88
102
|
}
|
|
89
103
|
};
|
|
90
104
|
|
|
91
|
-
const PACKAGE_NAME = '@fitlab-ai/agent-infra';
|
|
92
105
|
const AGENT_INFRA_SANDBOX_TOOL = 'agent-infra';
|
|
93
106
|
const LEGACY_DEFAULT_SANDBOX_TOOLS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
|
|
94
107
|
const DEFAULT_SANDBOX_TOOLS = [AGENT_INFRA_SANDBOX_TOOL, ...LEGACY_DEFAULT_SANDBOX_TOOLS];
|
|
@@ -151,6 +164,14 @@ function migrateSandboxTools(cfg) {
|
|
|
151
164
|
|
|
152
165
|
function norm(p) { return p.replace(/\\/g, '/'); }
|
|
153
166
|
|
|
167
|
+
function sha256(content) {
|
|
168
|
+
return `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function trustedBaseline(value) {
|
|
172
|
+
return typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value) ? value : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
154
175
|
function normDir(p) {
|
|
155
176
|
return norm(p).replace(/^\.\//, '').replace(/\/+$/, '');
|
|
156
177
|
}
|
|
@@ -839,117 +860,6 @@ function stripLangVariant(relativePath) {
|
|
|
839
860
|
return relativePath;
|
|
840
861
|
}
|
|
841
862
|
|
|
842
|
-
function isTemplateDir(dir) {
|
|
843
|
-
try {
|
|
844
|
-
return fs.statSync(dir).isDirectory();
|
|
845
|
-
} catch {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function verifyPackageDir(dir) {
|
|
851
|
-
const pkgPath = path.join(dir, 'package.json');
|
|
852
|
-
if (!fs.existsSync(pkgPath)) {
|
|
853
|
-
return { templateRoot: null, reason: `package.json not found at ${pkgPath}` };
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
let pkg;
|
|
857
|
-
try {
|
|
858
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
859
|
-
} catch {
|
|
860
|
-
return { templateRoot: null, reason: `invalid package.json at ${pkgPath}` };
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
if (pkg.name !== PACKAGE_NAME) {
|
|
864
|
-
const packageName = typeof pkg.name === 'string' && pkg.name ? pkg.name : 'an unknown package';
|
|
865
|
-
return { templateRoot: null, reason: `${pkgPath} belongs to ${packageName}` };
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const templateRoot = path.join(dir, 'templates');
|
|
869
|
-
if (!isTemplateDir(templateRoot)) {
|
|
870
|
-
return { templateRoot: null, reason: `templates/ not found at ${templateRoot}` };
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return { templateRoot, reason: null };
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function resolveUnixTemplateRoot(name) {
|
|
877
|
-
let linkPath;
|
|
878
|
-
try {
|
|
879
|
-
linkPath = childProcess.execSync(`command -v ${name}`, {
|
|
880
|
-
encoding: 'utf8',
|
|
881
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
882
|
-
}).trim();
|
|
883
|
-
} catch {
|
|
884
|
-
return { templateRoot: null, reason: 'not found in PATH' };
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
if (!linkPath) {
|
|
888
|
-
return { templateRoot: null, reason: 'not found in PATH' };
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
let realPath;
|
|
892
|
-
try {
|
|
893
|
-
realPath = fs.realpathSync(linkPath);
|
|
894
|
-
} catch {
|
|
895
|
-
return { templateRoot: null, reason: `cannot resolve symlink target for ${linkPath}` };
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
let dir = path.dirname(realPath);
|
|
899
|
-
while (true) {
|
|
900
|
-
const pkgPath = path.join(dir, 'package.json');
|
|
901
|
-
if (fs.existsSync(pkgPath)) {
|
|
902
|
-
return verifyPackageDir(dir);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const parentDir = path.dirname(dir);
|
|
906
|
-
if (parentDir === dir) {
|
|
907
|
-
break;
|
|
908
|
-
}
|
|
909
|
-
dir = parentDir;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
return { templateRoot: null, reason: `no package.json found above ${realPath}` };
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function resolveWindowsTemplateRoot(name) {
|
|
916
|
-
let output;
|
|
917
|
-
try {
|
|
918
|
-
output = childProcess.execSync(`where ${name}`, {
|
|
919
|
-
encoding: 'utf8',
|
|
920
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
921
|
-
}).trim();
|
|
922
|
-
} catch {
|
|
923
|
-
return { templateRoot: null, reason: 'not found in PATH' };
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const wrapperPaths = output.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
927
|
-
if (wrapperPaths.length === 0) {
|
|
928
|
-
return { templateRoot: null, reason: 'not found in PATH' };
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const wrapperPath = wrapperPaths.find(line => /\.cmd$/i.test(line)) || wrapperPaths[0];
|
|
932
|
-
const packageDir = path.join(path.dirname(wrapperPath), 'node_modules', '@fitlab-ai', 'agent-infra');
|
|
933
|
-
return verifyPackageDir(packageDir);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
function resolveTemplateRoot() {
|
|
937
|
-
const resolver = process.platform === 'win32'
|
|
938
|
-
? resolveWindowsTemplateRoot
|
|
939
|
-
: resolveUnixTemplateRoot;
|
|
940
|
-
const errors = [];
|
|
941
|
-
|
|
942
|
-
for (const name of ['ai', 'agent-infra']) {
|
|
943
|
-
const result = resolver(name);
|
|
944
|
-
if (result.templateRoot) {
|
|
945
|
-
return result.templateRoot;
|
|
946
|
-
}
|
|
947
|
-
errors.push({ name, reason: result.reason });
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
return { templateRoot: null, errors };
|
|
951
|
-
}
|
|
952
|
-
|
|
953
863
|
function isBinary(fp) {
|
|
954
864
|
const fd = fs.openSync(fp, 'r');
|
|
955
865
|
const buf = Buffer.alloc(8192);
|
|
@@ -1037,25 +947,14 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
1037
947
|
const configPathRel = norm(path.relative(projectRoot, cfgPath));
|
|
1038
948
|
let templateRoot = templateRootOverride;
|
|
1039
949
|
if (!templateRoot) {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
950
|
+
const packageResolution = resolveAgentInfraPackage({
|
|
951
|
+
startPath: path.join(projectRoot, '.agents', 'scripts', 'lib', 'agent-infra-package.js')
|
|
952
|
+
});
|
|
953
|
+
if (packageResolution.templateRoot && fs.existsSync(packageResolution.templateRoot)) {
|
|
954
|
+
templateRoot = packageResolution.templateRoot;
|
|
1043
955
|
} else {
|
|
1044
|
-
const details = resolvedTemplateRoot.errors
|
|
1045
|
-
.map(({ name, reason }) => ` - ${name}: ${reason}`)
|
|
1046
|
-
.join('\n');
|
|
1047
956
|
return {
|
|
1048
|
-
error:
|
|
1049
|
-
'Template source not found.',
|
|
1050
|
-
'',
|
|
1051
|
-
'Attempted binary lookups:',
|
|
1052
|
-
details,
|
|
1053
|
-
'',
|
|
1054
|
-
'Please ensure agent-infra is installed and available on PATH.',
|
|
1055
|
-
'If already installed, upgrade to the latest version or reinstall:',
|
|
1056
|
-
' npm install -g @fitlab-ai/agent-infra',
|
|
1057
|
-
' brew upgrade fitlab-ai/agent-infra/agent-infra || brew install fitlab-ai/agent-infra/agent-infra'
|
|
1058
|
-
].join('\n')
|
|
957
|
+
error: formatAgentInfraPackageError(packageResolution)
|
|
1059
958
|
};
|
|
1060
959
|
}
|
|
1061
960
|
}
|
|
@@ -1073,6 +972,19 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
1073
972
|
const managed = [...(cfg.files.managed || [])];
|
|
1074
973
|
const merged = [...(cfg.files.merged || [])];
|
|
1075
974
|
const ejected = [...(cfg.files.ejected || [])];
|
|
975
|
+
const guardedManaged = new Set((DEFAULTS.files.guardedManaged || []).map(norm));
|
|
976
|
+
const managedBaselines = cfg.files.managedBaselines && typeof cfg.files.managedBaselines === 'object'
|
|
977
|
+
&& !Array.isArray(cfg.files.managedBaselines)
|
|
978
|
+
? { ...cfg.files.managedBaselines }
|
|
979
|
+
: {};
|
|
980
|
+
let baselinesChanged = false;
|
|
981
|
+
|
|
982
|
+
for (const target of Object.keys(managedBaselines)) {
|
|
983
|
+
if (!guardedManaged.has(norm(target))) {
|
|
984
|
+
delete managedBaselines[target];
|
|
985
|
+
baselinesChanged = true;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
1076
988
|
|
|
1077
989
|
const report = {
|
|
1078
990
|
templateVersion: version,
|
|
@@ -1085,7 +997,17 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
1085
997
|
errors: [],
|
|
1086
998
|
conflicts: []
|
|
1087
999
|
},
|
|
1088
|
-
managed: {
|
|
1000
|
+
managed: {
|
|
1001
|
+
written: [],
|
|
1002
|
+
created: [],
|
|
1003
|
+
unchanged: [],
|
|
1004
|
+
protected: [],
|
|
1005
|
+
conflicts: [],
|
|
1006
|
+
skippedMerged: [],
|
|
1007
|
+
skippedPlatform: [],
|
|
1008
|
+
skippedTUI: [],
|
|
1009
|
+
removed: []
|
|
1010
|
+
},
|
|
1089
1011
|
custom: {
|
|
1090
1012
|
detected: [],
|
|
1091
1013
|
generated: [],
|
|
@@ -1127,6 +1049,22 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
1127
1049
|
const allRels = mergedRels;
|
|
1128
1050
|
const allSet = new Set(allRels);
|
|
1129
1051
|
|
|
1052
|
+
function renderedTemplate(entry) {
|
|
1053
|
+
const target = norm(renderPathname(entry, project));
|
|
1054
|
+
const selected = platformSelect(
|
|
1055
|
+
langSelect(entryVariantRels(entry, allSet, platformType), lang, allSet, project),
|
|
1056
|
+
platformType,
|
|
1057
|
+
project
|
|
1058
|
+
);
|
|
1059
|
+
const src = selected.get(target);
|
|
1060
|
+
if (!src) return null;
|
|
1061
|
+
const srcRoot = sourceMap.get(src) || templateRoot;
|
|
1062
|
+
const srcFull = path.join(srcRoot, src);
|
|
1063
|
+
return isBinary(srcFull)
|
|
1064
|
+
? fs.readFileSync(srcFull)
|
|
1065
|
+
: renderContent(fs.readFileSync(srcFull, 'utf8'), vars);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1130
1068
|
for (const entry of [...managed, ...merged, ...ejected]) {
|
|
1131
1069
|
if (!isPathOwnedByOtherPlatform(entry, platformType)) continue;
|
|
1132
1070
|
|
|
@@ -1135,17 +1073,49 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
1135
1073
|
if (!fs.existsSync(dir)) continue;
|
|
1136
1074
|
|
|
1137
1075
|
for (const filePath of walkDir(dir)) {
|
|
1076
|
+
const relativeFile = norm(path.relative(projectRoot, filePath));
|
|
1077
|
+
if (guardedManaged.has(relativeFile)) continue;
|
|
1138
1078
|
fs.unlinkSync(filePath);
|
|
1139
|
-
report.managed.removed.push(
|
|
1079
|
+
report.managed.removed.push(relativeFile);
|
|
1140
1080
|
}
|
|
1141
1081
|
removeEmptyDirs(dir);
|
|
1142
1082
|
continue;
|
|
1143
1083
|
}
|
|
1144
1084
|
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1085
|
+
const renderedTarget = norm(renderPathname(entry, project));
|
|
1086
|
+
const target = path.join(projectRoot, renderedTarget);
|
|
1087
|
+
if (!guardedManaged.has(renderedTarget)) {
|
|
1088
|
+
if (!fs.existsSync(target)) continue;
|
|
1089
|
+
fs.unlinkSync(target);
|
|
1090
|
+
report.managed.removed.push(renderedTarget);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const baseline = trustedBaseline(managedBaselines[renderedTarget]);
|
|
1095
|
+
const templateContent = renderedTemplate(entry);
|
|
1096
|
+
const templateHash = templateContent === null ? null : sha256(templateContent);
|
|
1097
|
+
const localHash = fs.existsSync(target) ? sha256(fs.readFileSync(target)) : null;
|
|
1098
|
+
const safeToRemove = localHash !== null && (localHash === baseline || (!baseline && localHash === templateHash));
|
|
1099
|
+
|
|
1100
|
+
if (safeToRemove) {
|
|
1101
|
+
fs.unlinkSync(target);
|
|
1102
|
+
report.managed.removed.push(renderedTarget);
|
|
1103
|
+
if (Object.prototype.hasOwnProperty.call(managedBaselines, renderedTarget)) {
|
|
1104
|
+
delete managedBaselines[renderedTarget];
|
|
1105
|
+
baselinesChanged = true;
|
|
1106
|
+
}
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (localHash !== null || baseline !== null) {
|
|
1111
|
+
report.managed.conflicts.push({
|
|
1112
|
+
target: renderedTarget,
|
|
1113
|
+
reason: localHash === null ? 'platform-switch-deleted' : 'platform-switch-modified',
|
|
1114
|
+
baseline,
|
|
1115
|
+
local: localHash,
|
|
1116
|
+
template: templateHash
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1149
1119
|
}
|
|
1150
1120
|
|
|
1151
1121
|
// Cleanup files owned by disabled built-in TUIs. Iterates managed + merged
|
|
@@ -1230,6 +1200,68 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
1230
1200
|
: renderContent(fs.readFileSync(srcFull, 'utf8'), vars);
|
|
1231
1201
|
|
|
1232
1202
|
const exists = fs.existsSync(dstFull);
|
|
1203
|
+
if (guardedManaged.has(tgt)) {
|
|
1204
|
+
const rawBaseline = managedBaselines[tgt];
|
|
1205
|
+
const baseline = trustedBaseline(rawBaseline);
|
|
1206
|
+
const templateHash = sha256(content);
|
|
1207
|
+
const localHash = exists ? sha256(fs.readFileSync(dstFull)) : null;
|
|
1208
|
+
|
|
1209
|
+
if (rawBaseline !== undefined && baseline === null) {
|
|
1210
|
+
delete managedBaselines[tgt];
|
|
1211
|
+
baselinesChanged = true;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (baseline === null && localHash !== null && localHash !== templateHash) {
|
|
1215
|
+
report.managed.conflicts.push({
|
|
1216
|
+
target: tgt,
|
|
1217
|
+
reason: 'unknown-origin',
|
|
1218
|
+
baseline: null,
|
|
1219
|
+
local: localHash,
|
|
1220
|
+
template: templateHash
|
|
1221
|
+
});
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (baseline !== null && templateHash === baseline && localHash !== baseline) {
|
|
1226
|
+
report.managed.protected.push({
|
|
1227
|
+
target: tgt,
|
|
1228
|
+
reason: localHash === null ? 'user-deleted' : 'user-modified',
|
|
1229
|
+
baseline,
|
|
1230
|
+
local: localHash,
|
|
1231
|
+
template: templateHash
|
|
1232
|
+
});
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (baseline !== null && localHash !== baseline && templateHash !== baseline && localHash !== templateHash) {
|
|
1237
|
+
report.managed.conflicts.push({
|
|
1238
|
+
target: tgt,
|
|
1239
|
+
reason: 'both-modified',
|
|
1240
|
+
baseline,
|
|
1241
|
+
local: localHash,
|
|
1242
|
+
template: templateHash
|
|
1243
|
+
});
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (localHash === templateHash) {
|
|
1248
|
+
report.managed.unchanged.push(tgt);
|
|
1249
|
+
if (managedBaselines[tgt] !== templateHash) {
|
|
1250
|
+
managedBaselines[tgt] = templateHash;
|
|
1251
|
+
baselinesChanged = true;
|
|
1252
|
+
}
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const dir = path.dirname(dstFull);
|
|
1257
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1258
|
+
fs.writeFileSync(dstFull, content);
|
|
1259
|
+
managedBaselines[tgt] = templateHash;
|
|
1260
|
+
baselinesChanged = true;
|
|
1261
|
+
(exists ? report.managed.written : report.managed.created).push(tgt);
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1233
1265
|
if (exists) {
|
|
1234
1266
|
const cur = bin ? fs.readFileSync(dstFull) : fs.readFileSync(dstFull, 'utf8');
|
|
1235
1267
|
if (bin ? content.equals(cur) : content === cur) {
|
|
@@ -1355,10 +1387,16 @@ function syncTemplates(projectRoot, templateRootOverride) {
|
|
|
1355
1387
|
cfg.files.managed = managed;
|
|
1356
1388
|
cfg.files.merged = merged;
|
|
1357
1389
|
cfg.files.ejected = ejected;
|
|
1390
|
+
if (Object.keys(managedBaselines).length > 0) {
|
|
1391
|
+
cfg.files.managedBaselines = managedBaselines;
|
|
1392
|
+
} else {
|
|
1393
|
+
if (Object.prototype.hasOwnProperty.call(cfg.files, 'managedBaselines')) baselinesChanged = true;
|
|
1394
|
+
delete cfg.files.managedBaselines;
|
|
1395
|
+
}
|
|
1358
1396
|
cfg.templateVersion = version;
|
|
1359
1397
|
delete cfg.templateSource;
|
|
1360
1398
|
|
|
1361
|
-
report.configUpdated = hasChanges || prevVersion !== version || hadTemplateSource || sandboxToolsMigrated;
|
|
1399
|
+
report.configUpdated = hasChanges || baselinesChanged || prevVersion !== version || hadTemplateSource || sandboxToolsMigrated;
|
|
1362
1400
|
|
|
1363
1401
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
1364
1402
|
|