@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.
- package/README.md +68 -0
- package/README.zh-CN.md +28 -0
- package/package.json +2 -2
- package/runtime/index.json +15 -15
- package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +11 -4
- package/runtime/plugins/agent-pack/plugin.json +1 -1
- package/runtime/plugins/agent-pack/references/capability-config.md +29 -5
- package/runtime/plugins/agent-pack/runtime/agent_pack_claude.py +448 -0
- package/runtime/plugins/agent-pack/runtime/agent_pack_config.py +178 -22
- package/runtime/plugins/agent-pack/runtime/agenthelm.py +237 -6
- package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +19 -0
- package/runtime/plugins/agent-pack/runtime/mcp_server.py +30 -8
- package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +2 -2
- package/runtime/plugins/agents-init/.codex-plugin/plugin.json +5 -5
- package/runtime/plugins/agents-init/content.json +15 -0
- package/runtime/plugins/agents-init/plugin.json +2 -2
- package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +14 -1
- package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +18 -3
- package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +43 -4
- package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +933 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +551 -0
- package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +64 -0
- package/runtime/plugins/agents-init/skills/agents-init/skill.json +3 -3
- package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +5 -5
- package/runtime/plugins/opinion-manager/plugin.json +1 -1
- package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +1 -1
- package/runtime/plugins/opinion-manager/skills/opinion-manager/agents/openai.yaml +2 -2
- package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +10 -4
- package/runtime/plugins/project-orchestrator/plugin.json +1 -1
- package/runtime/plugins/project-orchestrator/runtime/project_cli.py +69 -9
- package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +380 -27
- package/runtime/plugins/project-orchestrator/runtime/project_menu.py +18 -0
- package/runtime/plugins/project-orchestrator/runtime/project_real.py +7 -1
- package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +1 -1
- 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
|
|
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="
|
|
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": {
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
4
|
-
"description": "
|
|
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": "
|
|
12
|
-
"longDescription": "
|
|
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": "
|
|
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.
|
|
5
|
-
"description": "
|
|
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:
|
|
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 基线结构如下:
|