@sakanano-moe/agenthelm 6.6.0 → 6.8.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.
Files changed (35) hide show
  1. package/README.md +68 -0
  2. package/README.zh-CN.md +28 -0
  3. package/package.json +2 -2
  4. package/runtime/index.json +15 -15
  5. package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +11 -4
  6. package/runtime/plugins/agent-pack/plugin.json +1 -1
  7. package/runtime/plugins/agent-pack/references/capability-config.md +29 -5
  8. package/runtime/plugins/agent-pack/runtime/agent_pack_claude.py +448 -0
  9. package/runtime/plugins/agent-pack/runtime/agent_pack_config.py +178 -22
  10. package/runtime/plugins/agent-pack/runtime/agenthelm.py +237 -6
  11. package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +19 -0
  12. package/runtime/plugins/agent-pack/runtime/mcp_server.py +30 -8
  13. package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +2 -2
  14. package/runtime/plugins/agents-init/.codex-plugin/plugin.json +5 -5
  15. package/runtime/plugins/agents-init/content.json +15 -0
  16. package/runtime/plugins/agents-init/plugin.json +2 -2
  17. package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +14 -1
  18. package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +18 -3
  19. package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +43 -4
  20. package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +933 -0
  21. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +551 -0
  22. package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +64 -0
  23. package/runtime/plugins/agents-init/skills/agents-init/skill.json +3 -3
  24. package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +5 -5
  25. package/runtime/plugins/opinion-manager/plugin.json +1 -1
  26. package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +1 -1
  27. package/runtime/plugins/opinion-manager/skills/opinion-manager/agents/openai.yaml +2 -2
  28. package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +10 -4
  29. package/runtime/plugins/project-orchestrator/plugin.json +1 -1
  30. package/runtime/plugins/project-orchestrator/runtime/project_cli.py +69 -9
  31. package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +380 -27
  32. package/runtime/plugins/project-orchestrator/runtime/project_menu.py +18 -0
  33. package/runtime/plugins/project-orchestrator/runtime/project_real.py +7 -1
  34. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +1 -1
  35. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/agents/openai.yaml +2 -2
@@ -73,6 +73,10 @@ def write_json_atomic(path: Path, data: Any) -> None:
73
73
  os.replace(temporary, path)
74
74
 
75
75
 
76
+ def sha256_text(value: str) -> str:
77
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
78
+
79
+
76
80
  def sha256_file(path: Path) -> str:
77
81
  return hashlib.sha256(path.read_bytes()).hexdigest()
78
82
 
@@ -99,6 +103,11 @@ class Env:
99
103
  base = self.get("XDG_CACHE_HOME")
100
104
  return (Path(base) if base else self.home / ".cache") / "agenthelm"
101
105
 
106
+ @property
107
+ def state_dir(self) -> Path:
108
+ base = self.get("XDG_STATE_HOME")
109
+ return Path(base) if base else self.home / ".local" / "state"
110
+
102
111
  @property
103
112
  def config_file(self) -> Path:
104
113
  base = self.get("XDG_CONFIG_HOME")
@@ -356,10 +365,132 @@ def opinion_findings(project: Path, installed: list[dict]) -> list[dict]:
356
365
  return [{"status": "update-available" if result["updates"] else "current", "updates": result["updates"]}]
357
366
 
358
367
 
368
+ # ---------------------------------------------------------------- project scaffold
369
+
370
+ PROFILE_FILE = Path(".agents/moe.sakanano.agent-pack/project.json")
371
+ _SCAFFOLD_CACHE: dict[str, Any] = {}
372
+
373
+
374
+ def profile_root(project: Path | None) -> Path | None:
375
+ """The project root that holds the Project Profile, searching upwards from ``project``."""
376
+ if project is None:
377
+ return None
378
+ base = project.expanduser().resolve()
379
+ return next((path for path in [base, *base.parents] if (path / PROFILE_FILE).is_file()), None)
380
+
381
+
382
+ def scaffold_module(env: Env) -> Any | None:
383
+ """agents-init's scaffold module (the sibling copy first, then the newest installed one); None when too old."""
384
+ installed = installed_plugins(env, {"agents-init"})
385
+ copies = [PLUGIN_ROOT.parent / "agents-init", *(Path(item["path"]) for item in installed)]
386
+ usable = [path for path in copies if (path / "skills/agents-init/scripts/scaffold.py").is_file()]
387
+ if not usable:
388
+ return None
389
+ sibling = PLUGIN_ROOT.parent / "agents-init"
390
+ root = sibling if sibling in usable else max(usable, key=lambda p: version_key(_manifest_version(p) or "") or (0,))
391
+ scripts = root / "skills/agents-init/scripts"
392
+ key = str(scripts)
393
+ if key not in _SCAFFOLD_CACHE:
394
+ import importlib.util
395
+ spec = importlib.util.spec_from_file_location(f"agenthelm_scaffold_{len(_SCAFFOLD_CACHE)}", scripts / "scaffold.py")
396
+ module = importlib.util.module_from_spec(spec)
397
+ sys.modules[spec.name] = module
398
+ spec.loader.exec_module(module) # type: ignore[union-attr]
399
+ _SCAFFOLD_CACHE[key] = module
400
+ return _SCAFFOLD_CACHE[key]
401
+
402
+
403
+ def scaffold_report(env: Env, project: Path | None, *, reprompt: bool = False) -> dict | None:
404
+ """Read-only comparison of the project with the expected scaffold; None outside a project."""
405
+ root = profile_root(project)
406
+ if root is None:
407
+ return None
408
+ base = {"available": False, "root": str(root), "findings": [], "skipped": [], "error": ""}
409
+ module = scaffold_module(env)
410
+ if module is None:
411
+ return {**base, "error": "已安装的 agents-init 没有脚手架检查(更新 agents-init 后再检查)"}
412
+ try:
413
+ return {**module.check(root, state_dir=env.state_dir, reprompt=reprompt), "available": True,
414
+ "root": str(root), "error": ""}
415
+ except Exception as exc: # noqa: BLE001 - a broken Profile must not break plugin and content checks
416
+ return {**base, "error": str(exc)}
417
+
418
+
419
+ SYNC_SERVICES = (
420
+ ("iCloud Drive", re.compile(r"^(com~apple~CloudDocs|Mobile Documents|iCloud Drive|iCloudDrive)$")),
421
+ ("Dropbox", re.compile(r"^Dropbox( \(.*\))?$")),
422
+ ("OneDrive", re.compile(r"^OneDrive( - .*|-.*)?$")),
423
+ ("Google Drive", re.compile(r"^(GoogleDrive(-.*)?|Google Drive|My Drive)$")),
424
+ )
425
+ REBUILDABLE_DIRS = {"node_modules", ".venv", "venv", "__pycache__", ".pytest_cache", ".cache", "dist", "build", "target",
426
+ ".tox", ".next", ".gradle"}
427
+
428
+
429
+ def sync_folder(root: Path) -> dict | None:
430
+ """The sync service whose folder holds the project (by path), and the rebuildable directories inside it."""
431
+ resolved = root.resolve()
432
+ service = next((name for name, pattern in SYNC_SERVICES if any(pattern.match(part) for part in resolved.parts)), None)
433
+ if service is None:
434
+ return None
435
+ found: list[str] = []
436
+ for current, directories, _files in os.walk(resolved):
437
+ depth = len(Path(current).relative_to(resolved).parts)
438
+ descend = []
439
+ for name in directories:
440
+ if name in REBUILDABLE_DIRS:
441
+ found.append((Path(current) / name).relative_to(resolved).as_posix())
442
+ elif name != ".git" and depth < 1: # 只看根目录和它的下一层
443
+ descend.append(name)
444
+ directories[:] = descend
445
+ return {"service": service, "path": str(resolved), "rebuildable": sorted(found)}
446
+
447
+
448
+ def machine_findings(env: Env, project: Path | None, *, reprompt: bool = False) -> dict:
449
+ """Findings about this machine only (no user-level runtime ...); their decisions live in the user state directory."""
450
+ root = profile_root(project)
451
+ result: dict = {"findings": [], "skipped": []}
452
+ if root is None:
453
+ return result
454
+ import agenthelm_runtime # noqa: PLC0415 - shipped next to this file
455
+
456
+ findings = []
457
+ data = agenthelm_runtime.report(env, None)
458
+ if data["current"] is None:
459
+ text = agenthelm_runtime.missing_text(data, agenthelm_runtime.command_status())
460
+ findings.append({"id": "local:runtime-missing", "kind": "runtime-missing", "level": "suggest", "scope": "machine",
461
+ "title": "这台机器还没有安装用户级运行时", "reason": text["reason"], "impact": text["impact"],
462
+ "fingerprint": "runtime-missing", "fix": "install-runtime", "next": text["label"]})
463
+ where = sync_folder(root)
464
+ if where is not None:
465
+ extra = f";其中有可重建目录:{'、'.join(where['rebuildable'][:6])}" if where["rebuildable"] else ""
466
+ findings.append({"id": "local:sync-folder", "kind": "sync-folder", "level": "suggest", "scope": "machine",
467
+ "title": f"项目位于同步盘({where['service']})",
468
+ "reason": f"{where['path']}{extra}",
469
+ "impact": "多台机器共用同一份项目时:项目文件随同步盘走,但运行时、缓存与 wf 的台账是每台机器各一份;"
470
+ "可重建目录(依赖、构建产物)同步很慢,也容易产生冲突副本",
471
+ "fingerprint": sha256_text(where["service"]), "fix": "sync-folder",
472
+ "next": "运行 agenthelm fix sync-folder 查看指引(只给方案,不改文件、不删内容);不需要就 fix --skip local:sync-folder",
473
+ "sync": where})
474
+ module = scaffold_module(env)
475
+ if module is None or not findings:
476
+ result["findings"] = findings
477
+ return result
478
+ decisions = module.load_decisions(root, env.state_dir)
479
+ result["findings"], result["skipped"] = module.partition(findings, decisions, reprompt=reprompt)
480
+ return result
481
+
482
+
359
483
  # ---------------------------------------------------------------- report
360
484
 
485
+ def scaffold_fingerprint(env: Env, scaffold: dict | None) -> dict | None:
486
+ module = scaffold_module(env)
487
+ if scaffold is None or module is None or not scaffold.get("available"):
488
+ return None
489
+ return module.fingerprint(scaffold)
490
+
491
+
361
492
  def run_check(env: Env, project: Path | None, *, latest_source: str | None = None, offline: bool = False,
362
- max_age: int = CACHE_TTL_SECONDS, hook_mode: bool = False) -> dict:
493
+ max_age: int = CACHE_TTL_SECONDS, hook_mode: bool = False, reprompt: bool = False) -> dict:
363
494
  latest, origin = load_latest(
364
495
  env, latest_source, offline=offline, max_age=max_age,
365
496
  timeout=HOOK_FETCH_TIMEOUT_SECONDS if hook_mode else FETCH_TIMEOUT_SECONDS, backoff=hook_mode,
@@ -369,6 +500,9 @@ def run_check(env: Env, project: Path | None, *, latest_source: str | None = Non
369
500
  plugins = plugin_findings(latest, installed)
370
501
  content = content_findings(project, latest) if project else []
371
502
  opinion = opinion_findings(project, installed) if project else []
503
+ scaffold = scaffold_report(env, project, reprompt=reprompt)
504
+ machine = machine_findings(env, project, reprompt=reprompt)
505
+ scaffold_active = len(scaffold["findings"]) if scaffold else 0
372
506
  behind = [item for item in plugins if item["status"] == "behind"]
373
507
  legacy = [item for item in plugins if item["client"] == "legacy"]
374
508
  content_updates = [item for item in content if item["status"] in {"update-available", "conflict"}]
@@ -380,14 +514,18 @@ def run_check(env: Env, project: Path | None, *, latest_source: str | None = Non
380
514
  "plugins": plugins,
381
515
  "content": content,
382
516
  "opinion": opinion,
517
+ "scaffold": scaffold,
518
+ "machine": machine,
383
519
  "summary": {
520
+ "scaffold_findings": scaffold_active,
521
+ "scaffold_skipped": len(scaffold["skipped"]) if scaffold else 0,
384
522
  "plugins_behind": len(behind),
385
523
  "breaking": any(item["breaking"] for item in behind),
386
524
  "content_updates": len(content_updates),
387
525
  "content_conflicts": sum(1 for item in content if item["status"] == "conflict"),
388
526
  "opinion_updates": len(opinion_updates),
389
527
  "legacy_installs": len(legacy),
390
- "up_to_date": not (behind or content_updates or opinion_updates or legacy),
528
+ "up_to_date": not (behind or content_updates or opinion_updates or legacy or scaffold_active),
391
529
  },
392
530
  }
393
531
 
@@ -414,10 +552,52 @@ def one_line(report: dict) -> str | None:
414
552
  parts.append("Opinion 有新版本模板或规则")
415
553
  if summary["legacy_installs"]:
416
554
  parts.append(f"{summary['legacy_installs']} 个旧方式安装待迁移")
555
+ if summary["scaffold_findings"]:
556
+ parts.append(f"项目脚手架有 {summary['scaffold_findings']} 项差异")
417
557
  return "agenthelm:" + ",".join(parts) + f"。运行 `{CLI} check` 查看详情;更新前会先预览,由你决定。"
418
558
 
419
559
 
420
- def render_text(report: dict) -> str:
560
+ def render_scaffold(report: dict, show_skipped: bool) -> list[str]:
561
+ scaffold = report.get("scaffold")
562
+ lines = []
563
+ if scaffold is None:
564
+ return lines
565
+ if scaffold.get("error"):
566
+ lines.append(f" [脚手架] 无法检查:{scaffold['error']}")
567
+ return lines
568
+ for item in scaffold["findings"]:
569
+ mark = "(稍后)" if item.get("deferred") else "(期望内容已变化,重新提示)" if item.get("changed_since_decision") else ""
570
+ fix = f" 处理:fix {item['fix']}" if item.get("fix") else " 需要你自己处理"
571
+ lines.append(f" [脚手架] {item['title']}{mark}:{item['reason']}\n ID {item['id']}{fix}")
572
+ for message in scaffold["decisions"]["errors"]:
573
+ lines.append(f" [脚手架] 决定文件有问题:{message}")
574
+ if scaffold["decisions"]["conflict_copies"]:
575
+ lines.append(" [脚手架] 同步服务留下了决定文件的冲突副本:"
576
+ + "、".join(scaffold["decisions"]["conflict_copies"]) + "(合并到 check-decisions.json 后删除)")
577
+ if not scaffold["findings"] and not scaffold["decisions"]["errors"]:
578
+ lines.append(" [脚手架] 与期望一致" + (f"({len(scaffold['skipped'])} 项已跳过)" if scaffold["skipped"] else ""))
579
+ skipped = [*scaffold["skipped"], *report.get("machine", {}).get("skipped", [])]
580
+ if show_skipped:
581
+ for item in skipped:
582
+ decision = item["decision"]
583
+ lines.append(f" [已跳过·{decision['decision']}] {item['id']}:{decision.get('reason') or '(未写理由)'}"
584
+ f" {decision.get('decided_at', '')}")
585
+ if not skipped:
586
+ lines.append(" 没有被跳过的项。")
587
+ elif skipped:
588
+ lines.append(f" (另有 {len(skipped)} 项已被你跳过:check --show-skipped 查看)")
589
+ return lines
590
+
591
+
592
+ def render_machine(report: dict) -> list[str]:
593
+ lines = []
594
+ for item in report.get("machine", {}).get("findings", []):
595
+ lines.append(f" [本机] {item['title']}:{item['reason']}\n 影响:{item['impact']}\n 处理:{item['next']}"
596
+ f"\n ID {item['id']}(只记在这台机器上;fix --skip {item['id']} --reason …)")
597
+ return lines
598
+
599
+
600
+ def render_text(report: dict, *, show_skipped: bool = False) -> str:
421
601
  lines = []
422
602
  origin = report["latest"]["source"]
423
603
  lines.append(f"云端版本源:{origin.get('source')}" + ("(缓存)" if origin.get("cached") else ""))
@@ -460,6 +640,8 @@ def render_text(report: dict) -> str:
460
640
  lines.append(f" [Opinion] 有新版本:{refs}(用 opinion-manager 预览后确认)")
461
641
  elif item["status"] in {"error", "unknown"}:
462
642
  lines.append(f" [Opinion] {item.get('error')}")
643
+ lines += render_scaffold(report, show_skipped)
644
+ lines += render_machine(report)
463
645
  if not report["plugins"]:
464
646
  lines.append(f"运行 `{CLI} install` 预览安装。")
465
647
  else:
@@ -974,7 +1156,14 @@ def parser() -> argparse.ArgumentParser:
974
1156
  check.add_argument("--refresh", action="store_true", help="忽略一天的缓存,立即联网")
975
1157
  check.add_argument("--hook", action="store_true", help="会话开始时静默调用:有更新才输出一行")
976
1158
  check.add_argument("--hook-format", choices=("text", "claude-hook"), default="text")
977
- check.add_argument("--strict", action="store_true", help="有可更新项时退出码为 1(CI 用)")
1159
+ check.add_argument("--strict", action="store_true", help="有可更新项或脚手架差异时退出码为 1(CI 用)")
1160
+ check.add_argument("--show-skipped", action="store_true", help="列出被你跳过的项及理由")
1161
+ check.add_argument("--reprompt", action="store_true", help="忽略已记录的决定,重新评估全部脚手架差异")
1162
+ check.add_argument("--fingerprint", action="store_true",
1163
+ help="输出脚手架指纹(骨架状态 + 受检 Profile 字段 + 内容锁 + 命名规则;不含 Opinion、自有字段、已跳过项)")
1164
+ check.add_argument("--explain", nargs="?", const="-", metavar="对方的 JSON",
1165
+ help="与 --fingerprint 合用:不带参数时逐项列出这台的明细;带另一台机器 check --fingerprint "
1166
+ "--output json 的输出文件时,指出两台差在哪里(有差异退出码 1)")
978
1167
 
979
1168
  install = sub.add_parser("install", help="经各客户端的原生市场安装 agenthelm 插件(默认预览)")
980
1169
  common(install, project=False)
@@ -1004,6 +1193,8 @@ def parser() -> argparse.ArgumentParser:
1004
1193
  doctor_parser = sub.add_parser("doctor", help="报告重复安装、版本不一致、旧方式残留与检查开关")
1005
1194
  common(doctor_parser)
1006
1195
 
1196
+ sub.add_parser("fix", add_help=False,
1197
+ help="修复项目脚手架差异与更新(等同 project fix;默认预览,--apply 写入;--skip/--unskip 记录决定)")
1007
1198
  sub.add_parser("runtime", add_help=False,
1008
1199
  help="用户级运行时:install / update / use / status(日常只需要 Python,不经过 npx)")
1009
1200
  sub.add_parser("project", add_help=False,
@@ -1070,10 +1261,48 @@ def run_runtime(argv: list[str]) -> int:
1070
1261
  return agenthelm_runtime.main(argv, sys.modules[__name__])
1071
1262
 
1072
1263
 
1264
+ def print_fingerprint(env: Env, report: dict, args: argparse.Namespace) -> int:
1265
+ """``check --fingerprint [--explain [file]]``: the scaffold fingerprint, and where two machines differ."""
1266
+ result = scaffold_fingerprint(env, report.get("scaffold"))
1267
+ if result is None:
1268
+ reason = (report.get("scaffold") or {}).get("error") or "当前目录不在 agenthelm 项目里"
1269
+ print(json.dumps({"ok": False, "error": reason}, ensure_ascii=False) if args.output == "json"
1270
+ else f"无法计算脚手架指纹:{reason}", file=sys.stderr if args.output != "json" else sys.stdout)
1271
+ return 4
1272
+ payload: dict = {"ok": True, "fingerprint": result["fingerprint"], "detail": result["detail"]}
1273
+ lines = [f"脚手架指纹 {result['fingerprint']}"]
1274
+ differences: list[str] = []
1275
+ if args.explain and args.explain != "-":
1276
+ try:
1277
+ other = read_json(Path(args.explain).expanduser())
1278
+ except (OSError, ValueError) as exc:
1279
+ print(f"读不了对方的输出:{exc}", file=sys.stderr)
1280
+ return 2
1281
+ theirs = other.get("detail") or (other.get("scaffold_fingerprint") or {}).get("detail") or other.get("fingerprint", {}).get("detail")
1282
+ if not isinstance(theirs, dict):
1283
+ print("对方的文件里没有指纹明细:请在对方机器运行 agenthelm check --fingerprint --output json 并保存输出", file=sys.stderr)
1284
+ return 2
1285
+ module = scaffold_module(env)
1286
+ differences = module.explain(result["detail"], theirs)
1287
+ payload["same"] = not differences
1288
+ payload["differences"] = differences
1289
+ lines.append("两台机器的脚手架一致。" if not differences else f"有 {len(differences)} 处不同:")
1290
+ lines += [f" {item}" for item in differences]
1291
+ elif args.explain:
1292
+ lines += [f" {key} = {value}" for key, value in result["detail"].items()]
1293
+ if args.output == "json":
1294
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
1295
+ else:
1296
+ print("\n".join(lines))
1297
+ return 1 if differences else 0
1298
+
1299
+
1073
1300
  def main(argv: list[str] | None = None) -> int:
1074
1301
  argv = list(sys.argv[1:] if argv is None else argv)
1075
1302
  if argv[:1] == ["project"]:
1076
1303
  return run_project(argv[1:])
1304
+ if argv[:1] == ["fix"]:
1305
+ return run_project(["fix", *argv[1:]])
1077
1306
  if argv[:1] == ["runtime"]:
1078
1307
  return run_runtime(argv[1:])
1079
1308
  args = parser().parse_args(argv)
@@ -1094,12 +1323,14 @@ def main(argv: list[str] | None = None) -> int:
1094
1323
  try:
1095
1324
  if args.command == "check":
1096
1325
  report = run_check(env, project, latest_source=args.latest, offline=args.offline,
1097
- max_age=0 if args.refresh else CACHE_TTL_SECONDS)
1326
+ max_age=0 if args.refresh else CACHE_TTL_SECONDS, reprompt=args.reprompt)
1098
1327
  report.pop("_latest")
1328
+ if args.fingerprint or args.explain:
1329
+ return print_fingerprint(env, report, args)
1099
1330
  if args.output == "json":
1100
1331
  print(json.dumps(report, ensure_ascii=False, indent=2))
1101
1332
  else:
1102
- sys.stdout.write(render_text(report))
1333
+ sys.stdout.write(render_text(report, show_skipped=args.show_skipped))
1103
1334
  return 1 if args.strict and not report["summary"]["up_to_date"] else 0
1104
1335
  if args.command == "doctor":
1105
1336
  result = doctor(env, run_check(env, project, latest_source=args.latest))
@@ -157,6 +157,25 @@ def release_version(latest: dict | None) -> str | None:
157
157
  return tag[1:] if isinstance(tag, str) and tag.startswith("v") else tag
158
158
 
159
159
 
160
+ def command_status() -> dict:
161
+ """Which of the commands people type are on PATH right now (``wf`` is never installed by the runtime)."""
162
+ return {"agenthelm": shutil.which("agenthelm"), "wf": shutil.which("wf")}
163
+
164
+
165
+ def missing_text(data: dict, commands: dict) -> dict:
166
+ """Wording for “no user-level runtime installed”: says which commands work, instead of leaving “wf: not found” unexplained."""
167
+ def state(name: str) -> str:
168
+ return f"{name} 在 PATH 上({commands[name]})" if commands.get(name) else f"终端里的 {name} 命令目前不可用"
169
+ return {
170
+ "reason": f"本次由 {data['running_from']} 提供;{state('agenthelm')},{state('wf')}。",
171
+ "impact": "项目根目录的 ./menu 会通过客户端插件兜底,所以现在能用;"
172
+ "没有 AI 客户端或客户端插件被移除时打不开。wf 是单独的查找命令,运行时不会提供它:"
173
+ "用 ./menu find 词 或 agenthelm project find 词 代替",
174
+ "label": "安装运行时(agenthelm runtime install --apply,只写用户数据目录);"
175
+ "要在终端直接输入 agenthelm 时加 --link-bin ~/.local/bin(只链接 agenthelm,不含 wf)",
176
+ }
177
+
178
+
160
179
  def report(env: Any, latest: dict | None = None) -> dict:
161
180
  active = current(env)
162
181
  newest = release_version(latest)
@@ -43,7 +43,7 @@ TOOLS = [
43
43
  {
44
44
  "name": "agent_pack_bootstrap",
45
45
  "title": "Install Agent Plugin",
46
- "description": "Dry-run or install a portable plugin, including agent-pack itself, into Codex, Cursor, or Claude Code without overwriting an existing package.",
46
+ "description": "Dry-run or install a portable plugin, including agent-pack itself, into Codex, Cursor, or Claude Code without overwriting an existing package. For Claude Code, skills / mcps limit the install to those components, and local credential values on MCP entries are kept unless resetOverrides.",
47
47
  "inputSchema": {
48
48
  "type": "object",
49
49
  "properties": {
@@ -53,6 +53,9 @@ TOOLS = [
53
53
  "apply": {"type": "boolean", "default": False},
54
54
  "replace": {"type": "boolean", "default": False},
55
55
  "recoveryRoot": {"type": "string"},
56
+ "skills": {"type": "array", "items": {"type": "string"}, "default": []},
57
+ "mcps": {"type": "array", "items": {"type": "string"}, "default": []},
58
+ "resetOverrides": {"type": "boolean", "default": False},
56
59
  },
57
60
  "required": ["plugin", "client"],
58
61
  "additionalProperties": False,
@@ -62,12 +65,14 @@ TOOLS = [
62
65
  {
63
66
  "name": "agent_pack_reconcile",
64
67
  "title": "Reconcile Project and Client Capabilities",
65
- "description": "Classify capability drift, safely replace the managed project package, and archive explicitly selected legacy Skills or plugins with rollback. apply defaults to false.",
68
+ "description": "Classify capability drift, safely replace the managed project package, and archive explicitly selected legacy Skills or plugins with rollback. Claude Code gets its own package, MCP entries and Skill links; local credential values are kept unless resetOverrides. apply defaults to false.",
66
69
  "inputSchema": {
67
70
  "type": "object",
68
71
  "properties": {
69
72
  "project": {"type": "string"},
70
- "client": {"type": "string", "enum": ["codex", "cursor"]},
73
+ "client": {"type": "string", "enum": ["codex", "cursor", "claude"]},
74
+ "replace": {"type": "boolean", "default": False},
75
+ "resetOverrides": {"type": "boolean", "default": False},
71
76
  "home": {"type": "string"},
72
77
  "retireSkills": {"type": "array", "items": {"type": "string"}, "default": []},
73
78
  "retirePlugins": {"type": "array", "items": {"type": "string"}, "default": []},
@@ -97,10 +102,15 @@ TOOLS = [
97
102
  {
98
103
  "name": "agent_pack_doctor",
99
104
  "title": "Validate Project Capabilities",
100
- "description": "Validate an initialized project's Agent Plugin, Skill, MCP, duplicate-name, portability, and secret boundaries.",
105
+ "description": "Validate an initialized project's Agent Plugin, Skill, MCP, duplicate-name, portability, and secret boundaries. With clients, also report each client's missing, stale and different components and kept local overrides.",
101
106
  "inputSchema": {
102
107
  "type": "object",
103
- "properties": {"project": {"type": "string"}},
108
+ "properties": {
109
+ "project": {"type": "string"},
110
+ "clients": {"type": "array", "items": {"type": "string", "enum": ["codex", "cursor", "claude"]},
111
+ "default": []},
112
+ "home": {"type": "string"},
113
+ },
104
114
  "required": ["project"],
105
115
  "additionalProperties": False,
106
116
  },
@@ -127,14 +137,17 @@ TOOLS = [
127
137
  {
128
138
  "name": "agent_pack_sync",
129
139
  "title": "Sync Project Capabilities to an Agent Client",
130
- "description": "Dry-run or install a project's portable capability package into Codex or Cursor. apply defaults to false and existing targets are never overwritten.",
140
+ "description": "Dry-run or install a project's portable capability package into Codex, Cursor or Claude Code. apply defaults to false; replaced content goes to the recovery directory. Claude Code keeps local credential values unless resetOverrides, and only replaces entries it did not install with replace.",
131
141
  "inputSchema": {
132
142
  "type": "object",
133
143
  "properties": {
134
144
  "project": {"type": "string"},
135
- "client": {"type": "string", "enum": ["codex", "cursor"]},
145
+ "client": {"type": "string", "enum": ["codex", "cursor", "claude"]},
136
146
  "home": {"type": "string"},
137
147
  "apply": {"type": "boolean", "default": False},
148
+ "replace": {"type": "boolean", "default": False},
149
+ "resetOverrides": {"type": "boolean", "default": False},
150
+ "recoveryRoot": {"type": "string"},
138
151
  },
139
152
  "required": ["project", "client"],
140
153
  "additionalProperties": False,
@@ -174,6 +187,9 @@ def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
174
187
  apply=bool(arguments.get("apply", False)),
175
188
  replace=bool(arguments.get("replace", False)),
176
189
  recovery_root=recovery,
190
+ skills=arguments.get("skills", []),
191
+ mcps=arguments.get("mcps", []),
192
+ reset_overrides=bool(arguments.get("resetOverrides", False)),
177
193
  )
178
194
  if name == "agent_pack_reconcile":
179
195
  recovery_value = arguments.get("recoveryRoot")
@@ -187,6 +203,8 @@ def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
187
203
  recovery_root=recovery,
188
204
  apply=bool(arguments.get("apply", False)),
189
205
  retire_only=bool(arguments.get("retireOnly", False)),
206
+ replace=bool(arguments.get("replace", False)),
207
+ reset_overrides=bool(arguments.get("resetOverrides", False)),
190
208
  )
191
209
  if name == "agent_pack_inventory":
192
210
  inventory = inventory_source(arguments["source"], home)
@@ -194,7 +212,7 @@ def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
194
212
  payload["ok"] = not payload["issues"]
195
213
  return payload
196
214
  if name == "agent_pack_doctor":
197
- return doctor(Path(arguments["project"]))
215
+ return doctor(Path(arguments["project"]), arguments.get("clients", []), home)
198
216
  if name == "agent_pack_transfer":
199
217
  source = inventory_source(arguments["source"], home)
200
218
  return transfer_to_project(
@@ -205,11 +223,15 @@ def _call(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
205
223
  apply=bool(arguments.get("apply", False)),
206
224
  )
207
225
  if name == "agent_pack_sync":
226
+ recovery_value = arguments.get("recoveryRoot")
208
227
  return sync_project(
209
228
  Path(arguments["project"]),
210
229
  arguments["client"],
211
230
  home=home,
212
231
  apply=bool(arguments.get("apply", False)),
232
+ replace=bool(arguments.get("replace", False)),
233
+ reset_overrides=bool(arguments.get("resetOverrides", False)),
234
+ recovery_root=Path(recovery_value) if isinstance(recovery_value, str) else None,
213
235
  )
214
236
  raise AgentPackError(f"unknown tool: {name}")
215
237
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agent-pack
3
- description: 确定性地盘点、校验、安装、同步、迁移与对账 Agent Plugin、Skill 与 MCP server——在项目、插件目录与 Codex / Cursor / Claude 等客户端之间。当需要发现、导入、导出、同步或验证 Agent 能力,或诊断某台机器的扩展安装漂移时使用。项目生命周期(Task/Case/路由/验证/Git)不在本 Skill 内——那是 project-orchestrator。
3
+ description: Deterministically inventory, verify, install, sync, migrate and reconcile Agent Plugins, Skills and MCP servers across projects, plugin directories and clients such as Codex, Cursor and Claude. Use when discovering, importing, exporting, syncing or verifying Agent capabilities, or diagnosing extension drift on a machine. The project lifecycle (Task/Case/routing/verification/Git) is not part of this Skill; that is project-orchestrator. 中文:确定性地盘点、校验、安装、同步、迁移与对账 Agent Plugin、Skill 与 MCP server——在项目、插件目录与 Codex / Cursor / Claude 等客户端之间。当需要发现、导入、导出、同步或验证 Agent 能力,或诊断某台机器的扩展安装漂移时使用。项目生命周期(Task/Case/路由/验证/Git)不在本 Skill 内——那是 project-orchestrator。
4
4
  ---
5
5
 
6
6
  # Agent Pack(Agent 扩展能力管理)
@@ -29,7 +29,7 @@ description: 确定性地盘点、校验、安装、同步、迁移与对账 Age
29
29
  6. 只应用明确选择的组件;
30
30
  7. 对目标运行 `doctor`。
31
31
 
32
- 目标客户端尚未安装某包时,从可信本地插件根目录使用 `bootstrap`。`bootstrap` 支持 `codex` / `cursor` / `claude`;Claude 端写入 `~/.claude/skills` 符号链接与 `~/.claude.json` user-scope MCP,装完需重启 Claude Code。处理已有客户端时优先使用 `reconcile`:审阅组件状态,明确列出要退役的旧 Skill/plugin,再执行应用。只退役、不安装项目包时加 `--retire-only`。用户验收迁移前,已退役或替换的内容必须保留在报告的恢复目录(`~/.agent-pack/recovery/`)中。
32
+ 目标客户端尚未安装某包时,从可信本地插件根目录使用 `bootstrap`。`bootstrap` 支持 `codex` / `cursor` / `claude`;Claude 端写入 `~/.claude/skills` 符号链接与 `~/.claude.json` user-scope MCP,可用 `--skill` / `--mcp` 只装选中的组件,本机认证值默认保留(`--reset-overrides` 才丢弃),装完需重启 Claude Code。把项目能力同步到 Claude 用 `sync --client claude`:它为 Claude 单独生成包,只增删改本包装过的条目,不要用 `bootstrap` 指向 Codex 的包。核对客户端是否与项目一致用 `doctor --client <客户端>`。处理已有客户端时优先使用 `reconcile`:审阅组件状态,明确列出要退役的旧 Skill/plugin,再执行应用。只退役、不安装项目包时加 `--retire-only`。用户验收迁移前,已退役或替换的内容必须保留在报告的恢复目录(`~/.agent-pack/recovery/`)中。
33
33
 
34
34
  ## 打包与协议
35
35
 
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "agents-init",
3
- "version": "6.6.0",
4
- "description": "初始化或迁移 Agent 项目契约;同一分发包附带 project-orchestrator、agent-pack 与 opinion-manager。",
3
+ "version": "6.7.0",
4
+ "description": "Initialize or migrate an Agent project contract; ships with project-orchestrator, agent-pack and opinion-manager.",
5
5
  "author": {
6
6
  "name": "lc4t"
7
7
  },
8
8
  "skills": "./skills/",
9
9
  "interface": {
10
10
  "displayName": "Agents Init",
11
- "shortDescription": "初始化或迁移项目骨架与 Project Profile",
12
- "longDescription": "用 agents-init 创建或迁移可移植项目契约。project-orchestrator 管理项目生命周期,agent-pack 管理 Plugin/Skill/MCP,opinion-manager 管理用户与 Agent 之间的行为规则;三者作为独立 Plugin 随同分发。",
11
+ "shortDescription": "Initialize or migrate the project skeleton and Project Profile",
12
+ "longDescription": "Use agents-init to create or migrate a portable project contract, including a root launcher that opens the project menu without any AI agent. project-orchestrator manages the project lifecycle, agent-pack manages Plugins/Skills/MCP, and opinion-manager manages behavior rules between the user and the Agent; all three ship alongside it as separate plugins. 中文:用 agents-init 创建或迁移可移植项目契约(含不需要 AI Agent 就能打开项目菜单的根目录启动器)。project-orchestrator 管理项目生命周期,agent-pack 管理 Plugin/Skill/MCP,opinion-manager 管理用户与 Agent 之间的行为规则;三者作为独立 Plugin 随同分发。",
13
13
  "developerName": "lc4t",
14
14
  "category": "Developer Tools",
15
15
  "capabilities": [
@@ -17,6 +17,6 @@
17
17
  "project-migration",
18
18
  "opinion-configuration"
19
19
  ],
20
- "defaultPrompt": "使用 $agents-init 初始化或迁移项目,并通过 $opinion-manager 配置当前环境的 Agent 行为规则。"
20
+ "defaultPrompt": "Use $agents-init to initialize or migrate this project, then use $opinion-manager to configure the Agent behavior rules for this environment."
21
21
  }
22
22
  }
@@ -26,6 +26,21 @@
26
26
  "materializes_from_profile": [
27
27
  "entry.launcher"
28
28
  ]
29
+ },
30
+ {
31
+ "id": "agents-init/project-skeleton",
32
+ "version": "1.0.0",
33
+ "kind": "manifest",
34
+ "title": "期望骨架清单",
35
+ "description": "项目脚手架应有的文件、Profile 受检字段与不参与比较的字段(审计字段、Opinion 设置);agenthelm check 据此对比项目,不写入项目。",
36
+ "sources": [
37
+ "skills/agents-init/skeleton.json"
38
+ ],
39
+ "materializes": [],
40
+ "project_lock": "none",
41
+ "files": {
42
+ "skills/agents-init/skeleton.json": "70ea5e360f018c0d840d5d7aa022f811d665642a2d127f254f7a76335b4e77da"
43
+ }
29
44
  }
30
45
  ]
31
46
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "agents-init",
4
- "version": "6.6.0",
5
- "description": "初始化或迁移 Agent 项目契约;同一分发包附带 project-orchestrator、agent-pack 与 opinion-manager。",
4
+ "version": "6.7.0",
5
+ "description": "Initialize or migrate an Agent project contract (AGENTS.md, Project Profile, root launcher); ships with project-orchestrator, agent-pack and opinion-manager.",
6
6
  "author": {
7
7
  "name": "lc4t"
8
8
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agents-init
3
- description: 初始化仓库或将既有仓库迁移到 agents-init v6 项目骨架。用户要求初始化 Agent 协作、创建 AGENTS.md、增加 Project Profile(项目配置)或迁移旧版 agents-init 结构(含 v5 的 project-runtime Profile)时使用。本 Skill 只负责结构;project-orchestrator 负责项目生命周期,agent-pack 负责 Plugin/Skill/MCP,opinion-manager 负责用户确认的 Agent 行为规则。
3
+ description: Initialize a repository, or migrate an existing one, to the agents-init v6 project skeleton. Use when the user asks to set up Agent collaboration, create AGENTS.md, add a Project Profile, add a root launcher for the project menu, or migrate an older agents-init layout (including the v5 project-runtime Profile). This Skill only handles structure; project-orchestrator owns the project lifecycle, agent-pack owns Plugins/Skills/MCP, and opinion-manager owns user-confirmed Agent behavior rules. 中文:初始化仓库或将既有仓库迁移到 agents-init v6 项目骨架。用户要求初始化 Agent 协作、创建 AGENTS.md、增加 Project Profile(项目配置)或迁移旧版 agents-init 结构(含 v5 的 project-runtime Profile)时使用。本 Skill 只负责结构;project-orchestrator 负责项目生命周期,agent-pack 负责 Plugin/Skill/MCP,opinion-manager 负责用户确认的 Agent 行为规则。
4
4
  ---
5
5
 
6
6
  # Agents Init(项目初始化)
@@ -69,6 +69,19 @@ python3 scripts/init_project.py --mode migrate --project /path/to/project \
69
69
  --replace AGENTS.md --recovery-dir /safe/recovery/example-v6 --apply
70
70
  ```
71
71
 
72
+ 已初始化的项目对照最新脚手架用 `--mode check`(只读);其余模式默认只预览,`--apply` 才写入,已有文件不被覆盖,替换前备份到 `--recovery-dir`:
73
+
74
+ ```bash
75
+ python3 scripts/init_project.py --project /path/to/project --mode check
76
+ python3 scripts/init_project.py --project /path/to/project --mode add-skeleton [--target docs/refs/README.md]
77
+ python3 scripts/init_project.py --project /path/to/project --mode profile [--target runtime.commit_policy] --recovery-dir /safe/recovery/example
78
+ python3 scripts/init_project.py --project /path/to/project --mode repair-lock
79
+ python3 scripts/init_project.py --project /path/to/project --mode decide --target skel:docs/drafts/.gitkeep --decision skip --reason "…" --apply
80
+ python3 scripts/init_project.py --project /path/to/project --mode case-naming # 只给改名计划,不移动目录
81
+ ```
82
+
83
+ 期望骨架清单是 [skeleton.json](skeleton.json)(内容包 `agents-init/project-skeleton`);Opinion、Profile 里的项目自有字段与审计字段不参与比较。用户的跳过 / 保留决定写入 `.agents/moe.sakanano.agent-pack/check-decisions.json`,随项目同步。日常入口是 `agenthelm check` 与 `agenthelm fix`。
84
+
72
85
  ## 输出契约
73
86
 
74
87
  v6 基线结构如下: