@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.
Files changed (122) hide show
  1. package/README.zh-CN.md +10 -0
  2. package/bin/cli.ts +1 -1
  3. package/dist/bin/cli.js +1 -1
  4. package/dist/lib/cp.js +56 -15
  5. package/dist/lib/defaults.json +9 -0
  6. package/dist/lib/init.js +5 -0
  7. package/dist/lib/sandbox/capture.js +5 -3
  8. package/dist/lib/sandbox/clipboard/bridge.js +12 -9
  9. package/dist/lib/sandbox/clipboard/inbox.js +82 -0
  10. package/dist/lib/sandbox/commands/create.js +68 -26
  11. package/dist/lib/sandbox/engine.js +33 -3
  12. package/dist/lib/sandbox/engines/colima.js +2 -2
  13. package/dist/lib/sandbox/runtimes/base.dockerfile +7 -0
  14. package/dist/lib/sandbox/shell.js +9 -1
  15. package/dist/lib/sandbox/tools.js +2 -2
  16. package/dist/lib/server/daemon.js +9 -0
  17. package/dist/lib/server/process-control.js +66 -73
  18. package/dist/lib/server/process-state.js +168 -0
  19. package/dist/lib/update.js +2 -0
  20. package/lib/cp.ts +56 -15
  21. package/lib/defaults.json +9 -0
  22. package/lib/init.ts +9 -0
  23. package/lib/sandbox/capture.ts +5 -3
  24. package/lib/sandbox/clipboard/bridge.ts +14 -12
  25. package/lib/sandbox/clipboard/inbox.ts +83 -0
  26. package/lib/sandbox/commands/create.ts +80 -27
  27. package/lib/sandbox/engine.ts +37 -3
  28. package/lib/sandbox/engines/colima.ts +2 -2
  29. package/lib/sandbox/runtimes/base.dockerfile +7 -0
  30. package/lib/sandbox/shell.ts +11 -2
  31. package/lib/sandbox/tools.ts +5 -5
  32. package/lib/server/daemon.ts +9 -0
  33. package/lib/server/process-control.ts +70 -71
  34. package/lib/server/process-state.ts +183 -0
  35. package/lib/update.ts +6 -0
  36. package/package.json +2 -1
  37. package/runtime/platform-adapters/platform-sync.github.js +1501 -0
  38. package/templates/.agents/README.en.md +5 -0
  39. package/templates/.agents/README.zh-CN.md +5 -0
  40. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  41. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  42. package/templates/.agents/rules/review-handshake.en.md +5 -4
  43. package/templates/.agents/rules/review-handshake.zh-CN.md +5 -4
  44. package/templates/.agents/rules/testing-discipline.en.md +3 -41
  45. package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
  46. package/templates/.agents/scripts/lib/agent-infra-package.js +149 -0
  47. package/templates/.agents/scripts/lib/post-review-commit.js +5 -0
  48. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +25 -1459
  49. package/templates/.agents/scripts/review-diff-fingerprint.js +95 -24
  50. package/templates/.agents/scripts/validate-artifact.js +144 -38
  51. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  52. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  53. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
  54. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  55. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  56. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  57. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  58. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
  59. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  60. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  61. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  62. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  63. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  64. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  65. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +2 -2
  66. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
  67. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
  68. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
  69. package/templates/.agents/skills/commit/SKILL.en.md +6 -4
  70. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -4
  71. package/templates/.agents/skills/commit/reference/task-status-update.en.md +35 -8
  72. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +35 -8
  73. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +27 -3
  74. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +27 -3
  75. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  76. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  77. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  78. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  79. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  80. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  81. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  82. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  83. package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
  84. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  85. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
  86. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
  87. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  88. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
  89. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
  90. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
  91. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
  92. package/templates/.agents/skills/init-milestones/SKILL.en.md +5 -4
  93. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +6 -5
  94. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +170 -45
  95. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  96. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  97. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  98. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  99. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  100. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  101. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
  102. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
  103. package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
  104. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
  105. package/templates/.agents/skills/review-code/config/verify.en.json +3 -1
  106. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +3 -1
  107. package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
  108. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
  109. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -2
  110. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -2
  111. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  112. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  113. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
  114. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
  115. package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
  116. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -1
  117. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +6 -2
  118. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +203 -142
  119. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  120. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  121. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
  122. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -1
@@ -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,14 +71,23 @@ 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.*",
73
87
  "**/test-integration.*",
74
88
  "**/test.*",
75
89
  "**/upgrade-dependency.*",
90
+ ".agents/rules/testing-discipline.*",
76
91
  ".agents/skills/post-release/SKILL.*",
77
92
  ".agents/skills/release/SKILL.*",
78
93
  ".agents/skills/test-integration/SKILL.*",
@@ -87,7 +102,6 @@ const DEFAULTS = {
87
102
  }
88
103
  };
89
104
 
90
- const PACKAGE_NAME = '@fitlab-ai/agent-infra';
91
105
  const AGENT_INFRA_SANDBOX_TOOL = 'agent-infra';
92
106
  const LEGACY_DEFAULT_SANDBOX_TOOLS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
93
107
  const DEFAULT_SANDBOX_TOOLS = [AGENT_INFRA_SANDBOX_TOOL, ...LEGACY_DEFAULT_SANDBOX_TOOLS];
@@ -150,6 +164,14 @@ function migrateSandboxTools(cfg) {
150
164
 
151
165
  function norm(p) { return p.replace(/\\/g, '/'); }
152
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
+
153
175
  function normDir(p) {
154
176
  return norm(p).replace(/^\.\//, '').replace(/\/+$/, '');
155
177
  }
@@ -235,7 +257,7 @@ function parseSkillFrontmatter(filePath) {
235
257
  if (!pair) continue;
236
258
 
237
259
  const [, key, rawValue] = pair;
238
- if (rawValue === '>') {
260
+ if (rawValue === '>' || rawValue === '|') {
239
261
  const block = [];
240
262
  for (let offset = index + 1; offset < lines.length; offset += 1) {
241
263
  const nextLine = lines[offset];
@@ -244,7 +266,7 @@ function parseSkillFrontmatter(filePath) {
244
266
  block.push(nextLine.trim());
245
267
  index = offset;
246
268
  }
247
- result[key] = block.join(' ').trim();
269
+ result[key] = block.join(rawValue === '|' ? '\n' : ' ').trim();
248
270
  continue;
249
271
  }
250
272
 
@@ -286,7 +308,8 @@ function detectCustomSkills(projectRoot, templateSkillNames) {
286
308
  dirName: entry.name,
287
309
  name: meta.name || entry.name,
288
310
  description: meta.description || '',
289
- args: meta.args || null
311
+ args: meta.args || null,
312
+ disableModelInvocation: meta['disable-model-invocation'] === 'true'
290
313
  };
291
314
  })
292
315
  .filter(Boolean)
@@ -501,14 +524,35 @@ function cleanStaleSyncedFiles(projectRoot, syncedSkills, report) {
501
524
  }
502
525
  }
503
526
 
527
+ function formatYamlMetadata(key, value) {
528
+ if (!value.includes('\n')) {
529
+ return [`${key}: ${JSON.stringify(value)}`];
530
+ }
531
+
532
+ return [`${key}: |-`, ...value.split('\n').map((line) => ` ${line}`)];
533
+ }
534
+
535
+ function formatTomlMetadata(key, value) {
536
+ if (!value.includes('\n')) {
537
+ return `${key} = ${JSON.stringify(value)}`;
538
+ }
539
+
540
+ const lines = value.split('\n').map((line) => JSON.stringify(line).slice(1, -1));
541
+ return `${key} = """${lines.join('\n')}"""`;
542
+ }
543
+
504
544
  function generateClaudeCommand(skill, lang) {
505
545
  const isZhCN = lang === 'zh-CN';
506
- const lines = ['---', `description: ${JSON.stringify(skill.description)}`];
546
+ const lines = ['---', ...formatYamlMetadata('description', skill.description)];
507
547
 
508
548
  if (skill.args) {
509
549
  lines.push(`usage: ${JSON.stringify(`/${skill.dirName} ${skill.args}`)}`);
510
550
  }
511
551
 
552
+ if (skill.disableModelInvocation) {
553
+ lines.push('disable-model-invocation: true');
554
+ }
555
+
512
556
  lines.push('---', '');
513
557
  lines.push(
514
558
  isZhCN
@@ -539,7 +583,7 @@ function generateGeminiCommand(skill, lang) {
539
583
  promptLines.push(isZhCN ? '严格按照技能中定义的所有步骤执行。' : 'Follow all steps defined in the skill exactly.');
540
584
 
541
585
  return [
542
- `description = ${JSON.stringify(skill.description)}`,
586
+ formatTomlMetadata('description', skill.description),
543
587
  'prompt = """',
544
588
  ...promptLines,
545
589
  '"""'
@@ -550,7 +594,7 @@ function generateOpenCodeCommand(skill, lang) {
550
594
  const isZhCN = lang === 'zh-CN';
551
595
  const lines = [
552
596
  '---',
553
- `description: ${JSON.stringify(skill.description)}`,
597
+ ...formatYamlMetadata('description', skill.description),
554
598
  'agent: general',
555
599
  'subtask: false',
556
600
  '---',
@@ -816,117 +860,6 @@ function stripLangVariant(relativePath) {
816
860
  return relativePath;
817
861
  }
818
862
 
819
- function isTemplateDir(dir) {
820
- try {
821
- return fs.statSync(dir).isDirectory();
822
- } catch {
823
- return false;
824
- }
825
- }
826
-
827
- function verifyPackageDir(dir) {
828
- const pkgPath = path.join(dir, 'package.json');
829
- if (!fs.existsSync(pkgPath)) {
830
- return { templateRoot: null, reason: `package.json not found at ${pkgPath}` };
831
- }
832
-
833
- let pkg;
834
- try {
835
- pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
836
- } catch {
837
- return { templateRoot: null, reason: `invalid package.json at ${pkgPath}` };
838
- }
839
-
840
- if (pkg.name !== PACKAGE_NAME) {
841
- const packageName = typeof pkg.name === 'string' && pkg.name ? pkg.name : 'an unknown package';
842
- return { templateRoot: null, reason: `${pkgPath} belongs to ${packageName}` };
843
- }
844
-
845
- const templateRoot = path.join(dir, 'templates');
846
- if (!isTemplateDir(templateRoot)) {
847
- return { templateRoot: null, reason: `templates/ not found at ${templateRoot}` };
848
- }
849
-
850
- return { templateRoot, reason: null };
851
- }
852
-
853
- function resolveUnixTemplateRoot(name) {
854
- let linkPath;
855
- try {
856
- linkPath = childProcess.execSync(`command -v ${name}`, {
857
- encoding: 'utf8',
858
- stdio: ['pipe', 'pipe', 'pipe']
859
- }).trim();
860
- } catch {
861
- return { templateRoot: null, reason: 'not found in PATH' };
862
- }
863
-
864
- if (!linkPath) {
865
- return { templateRoot: null, reason: 'not found in PATH' };
866
- }
867
-
868
- let realPath;
869
- try {
870
- realPath = fs.realpathSync(linkPath);
871
- } catch {
872
- return { templateRoot: null, reason: `cannot resolve symlink target for ${linkPath}` };
873
- }
874
-
875
- let dir = path.dirname(realPath);
876
- while (true) {
877
- const pkgPath = path.join(dir, 'package.json');
878
- if (fs.existsSync(pkgPath)) {
879
- return verifyPackageDir(dir);
880
- }
881
-
882
- const parentDir = path.dirname(dir);
883
- if (parentDir === dir) {
884
- break;
885
- }
886
- dir = parentDir;
887
- }
888
-
889
- return { templateRoot: null, reason: `no package.json found above ${realPath}` };
890
- }
891
-
892
- function resolveWindowsTemplateRoot(name) {
893
- let output;
894
- try {
895
- output = childProcess.execSync(`where ${name}`, {
896
- encoding: 'utf8',
897
- stdio: ['pipe', 'pipe', 'pipe']
898
- }).trim();
899
- } catch {
900
- return { templateRoot: null, reason: 'not found in PATH' };
901
- }
902
-
903
- const wrapperPaths = output.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
904
- if (wrapperPaths.length === 0) {
905
- return { templateRoot: null, reason: 'not found in PATH' };
906
- }
907
-
908
- const wrapperPath = wrapperPaths.find(line => /\.cmd$/i.test(line)) || wrapperPaths[0];
909
- const packageDir = path.join(path.dirname(wrapperPath), 'node_modules', '@fitlab-ai', 'agent-infra');
910
- return verifyPackageDir(packageDir);
911
- }
912
-
913
- function resolveTemplateRoot() {
914
- const resolver = process.platform === 'win32'
915
- ? resolveWindowsTemplateRoot
916
- : resolveUnixTemplateRoot;
917
- const errors = [];
918
-
919
- for (const name of ['ai', 'agent-infra']) {
920
- const result = resolver(name);
921
- if (result.templateRoot) {
922
- return result.templateRoot;
923
- }
924
- errors.push({ name, reason: result.reason });
925
- }
926
-
927
- return { templateRoot: null, errors };
928
- }
929
-
930
863
  function isBinary(fp) {
931
864
  const fd = fs.openSync(fp, 'r');
932
865
  const buf = Buffer.alloc(8192);
@@ -1014,25 +947,14 @@ function syncTemplates(projectRoot, templateRootOverride) {
1014
947
  const configPathRel = norm(path.relative(projectRoot, cfgPath));
1015
948
  let templateRoot = templateRootOverride;
1016
949
  if (!templateRoot) {
1017
- const resolvedTemplateRoot = resolveTemplateRoot();
1018
- if (typeof resolvedTemplateRoot === 'string') {
1019
- templateRoot = resolvedTemplateRoot;
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;
1020
955
  } else {
1021
- const details = resolvedTemplateRoot.errors
1022
- .map(({ name, reason }) => ` - ${name}: ${reason}`)
1023
- .join('\n');
1024
956
  return {
1025
- error: [
1026
- 'Template source not found.',
1027
- '',
1028
- 'Attempted binary lookups:',
1029
- details,
1030
- '',
1031
- 'Please ensure agent-infra is installed and available on PATH.',
1032
- 'If already installed, upgrade to the latest version or reinstall:',
1033
- ' npm install -g @fitlab-ai/agent-infra',
1034
- ' brew upgrade fitlab-ai/agent-infra/agent-infra || brew install fitlab-ai/agent-infra/agent-infra'
1035
- ].join('\n')
957
+ error: formatAgentInfraPackageError(packageResolution)
1036
958
  };
1037
959
  }
1038
960
  }
@@ -1050,6 +972,19 @@ function syncTemplates(projectRoot, templateRootOverride) {
1050
972
  const managed = [...(cfg.files.managed || [])];
1051
973
  const merged = [...(cfg.files.merged || [])];
1052
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
+ }
1053
988
 
1054
989
  const report = {
1055
990
  templateVersion: version,
@@ -1062,7 +997,17 @@ function syncTemplates(projectRoot, templateRootOverride) {
1062
997
  errors: [],
1063
998
  conflicts: []
1064
999
  },
1065
- managed: { written: [], created: [], unchanged: [], skippedMerged: [], skippedPlatform: [], skippedTUI: [], removed: [] },
1000
+ managed: {
1001
+ written: [],
1002
+ created: [],
1003
+ unchanged: [],
1004
+ protected: [],
1005
+ conflicts: [],
1006
+ skippedMerged: [],
1007
+ skippedPlatform: [],
1008
+ skippedTUI: [],
1009
+ removed: []
1010
+ },
1066
1011
  custom: {
1067
1012
  detected: [],
1068
1013
  generated: [],
@@ -1104,6 +1049,22 @@ function syncTemplates(projectRoot, templateRootOverride) {
1104
1049
  const allRels = mergedRels;
1105
1050
  const allSet = new Set(allRels);
1106
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
+
1107
1068
  for (const entry of [...managed, ...merged, ...ejected]) {
1108
1069
  if (!isPathOwnedByOtherPlatform(entry, platformType)) continue;
1109
1070
 
@@ -1112,17 +1073,49 @@ function syncTemplates(projectRoot, templateRootOverride) {
1112
1073
  if (!fs.existsSync(dir)) continue;
1113
1074
 
1114
1075
  for (const filePath of walkDir(dir)) {
1076
+ const relativeFile = norm(path.relative(projectRoot, filePath));
1077
+ if (guardedManaged.has(relativeFile)) continue;
1115
1078
  fs.unlinkSync(filePath);
1116
- report.managed.removed.push(norm(path.relative(projectRoot, filePath)));
1079
+ report.managed.removed.push(relativeFile);
1117
1080
  }
1118
1081
  removeEmptyDirs(dir);
1119
1082
  continue;
1120
1083
  }
1121
1084
 
1122
- const target = path.join(projectRoot, renderPathname(entry, project));
1123
- if (!fs.existsSync(target)) continue;
1124
- fs.unlinkSync(target);
1125
- report.managed.removed.push(norm(path.relative(projectRoot, target)));
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
+ }
1126
1119
  }
1127
1120
 
1128
1121
  // Cleanup files owned by disabled built-in TUIs. Iterates managed + merged
@@ -1207,6 +1200,68 @@ function syncTemplates(projectRoot, templateRootOverride) {
1207
1200
  : renderContent(fs.readFileSync(srcFull, 'utf8'), vars);
1208
1201
 
1209
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
+
1210
1265
  if (exists) {
1211
1266
  const cur = bin ? fs.readFileSync(dstFull) : fs.readFileSync(dstFull, 'utf8');
1212
1267
  if (bin ? content.equals(cur) : content === cur) {
@@ -1332,10 +1387,16 @@ function syncTemplates(projectRoot, templateRootOverride) {
1332
1387
  cfg.files.managed = managed;
1333
1388
  cfg.files.merged = merged;
1334
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
+ }
1335
1396
  cfg.templateVersion = version;
1336
1397
  delete cfg.templateSource;
1337
1398
 
1338
- report.configUpdated = hasChanges || prevVersion !== version || hadTemplateSource || sandboxToolsMigrated;
1399
+ report.configUpdated = hasChanges || baselinesChanged || prevVersion !== version || hadTemplateSource || sandboxToolsMigrated;
1339
1400
 
1340
1401
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
1341
1402
 
@@ -67,7 +67,7 @@ When self-heal hits the cap, the failure is non-code, the run id is unlocatable,
67
67
  Get the current time:
68
68
 
69
69
  ```bash
70
- date "+%Y-%m-%d %H:%M:%S%:z"
70
+ date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'
71
71
  ```
72
72
 
73
73
  Update `.agents/workspace/active/{task-id}/task.md`:
@@ -109,7 +109,7 @@ Output per scenario:
109
109
  ```
110
110
  Next step - Complete and archive the task:
111
111
  - Claude Code / OpenCode: /complete-task {task-ref}
112
- - Gemini CLI: /agent-infra:complete-task {task-ref}
112
+ - Gemini CLI: /{{project}}:complete-task {task-ref}
113
113
  - Codex CLI: $complete-task {task-ref}
114
114
  ```
115
115
 
@@ -67,7 +67,7 @@ description: >
67
67
  获取当前时间:
68
68
 
69
69
  ```bash
70
- date "+%Y-%m-%d %H:%M:%S%:z"
70
+ date "+%Y-%m-%d %H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/'
71
71
  ```
72
72
 
73
73
  更新 `.agents/workspace/active/{task-id}/task.md`:
@@ -109,7 +109,7 @@ node .agents/scripts/validate-artifact.js gate watch-pr .agents/workspace/active
109
109
  ```
110
110
  下一步 - 完成并归档任务:
111
111
  - Claude Code / OpenCode:/complete-task {task-ref}
112
- - Gemini CLI:/agent-infra:complete-task {task-ref}
112
+ - Gemini CLI:/{{project}}:complete-task {task-ref}
113
113
  - Codex CLI:$complete-task {task-ref}
114
114
  ```
115
115
 
@@ -12,6 +12,13 @@ After running the watch command from `.agents/rules/pr-checks-commands.md`, clas
12
12
 
13
13
  ## Self-Heal Decision Tree
14
14
 
15
+ ```text
16
+ # self-heal-test-command-contract
17
+ primary: failing-job-command
18
+ fallback-source: project-test-skill
19
+ unknown: help
20
+ ```
21
+
15
22
  For each failing check, decide "self-heal" vs "ask for help" in this order:
16
23
 
17
24
  1. **Can the corresponding CI run be located** (per the rule's "Resolve a failing run id")? No → ask for help.
@@ -22,7 +29,7 @@ For each failing check, decide "self-heal" vs "ask for help" in this order:
22
29
  4. When "locatable + code layer + under cap" holds, perform one self-heal:
23
30
  - Before fixing, run `git status -s` to record the working tree and ensure only changes related to this failure are included.
24
31
  - Locate and make a minimal fix per the logs (touch only code / tests / config related to that failure).
25
- - Run the relevant tests: prefer the local command for the failing job; fall back to `npm run test:core` repo-wide. **Do not commit or push before tests pass.**
32
+ - Run the relevant tests: prefer the local command for the failing job; otherwise read the project `test` skill and select its declared core or full validation command. If neither command is known, ask for help. **Do not commit or push before tests pass.**
26
33
  - After tests pass, publish the fix: per `.agents/rules/commit-and-pr.md` stage only the related files (`git add <paths>`, avoid `git add -A` pulling in unrelated changes) → create the fix commit (`git commit`) → `git push` to the current PR branch.
27
34
  - Record the fix commit SHA, increment the fix count, and return to SKILL step 2 to re-watch.
28
35
  - Never make unrelated "drive-by" optimizations; never loosen / skip the failing assertion to "make it green".
@@ -12,6 +12,13 @@
12
12
 
13
13
  ## 自愈决策树
14
14
 
15
+ ```text
16
+ # self-heal-test-command-contract
17
+ primary: failing-job-command
18
+ fallback-source: project-test-skill
19
+ unknown: help
20
+ ```
21
+
15
22
  对每个失败 check,按下列顺序判定「自愈」还是「求助」:
16
23
 
17
24
  1. **能否定位到对应的 CI run**(按规则的「解析失败 run id」)?否 → 求助。
@@ -22,7 +29,7 @@
22
29
  4. 满足「可定位 + 代码层 + 未达上限」时执行一次自愈:
23
30
  - 自愈前先 `git status -s` 记录当前工作树,确保后续只纳入与本次失败相关的改动。
24
31
  - 在本地按日志定位并最小化修复(只动与该失败相关的代码 / 测试 / 配置)。
25
- - 运行对应测试:优先失败 job 对应的本地命令;仓库统一回退 `npm run test:core`。**测试通过前不得提交或推送。**
32
+ - 运行对应测试:优先失败 job 对应的本地命令;否则读取项目 `test` skill,选择其中声明的 core 或 full 验证命令。两者均未知时进入求助出口。**测试通过前不得提交或推送。**
26
33
  - 测试通过后发布修复:按 `.agents/rules/commit-and-pr.md` 仅暂存本次相关文件(`git add <相关路径>`,避免 `git add -A` 卷入无关改动)→ 创建修复 commit(`git commit`)→ `git push` 到当前 PR 分支。
27
34
  - 记录本次修复的 commit SHA,修复计数 +1,回到 SKILL 步骤 2 重新监控。
28
35
  - 绝不执行与失败无关的「顺手优化」;不放宽 / 跳过失败的断言来「修绿」。