@viruspc/edges
v0.2.7
Published
Multi-command Edges CLI (notes, tasks, skills, and more)
Readme
Edges CLI
edges 是人和 Agent 共用的 Edges 命令行入口。它把任务、项目记忆、笔记和发布能力放到同一套作用域与节点协议上,让能力可以跨仓库复用,而内容始终归属于选定的目录。
核心分工是:先确定作用域,由 Service 完成用例,Model 维护单节点规则,operations 处理集合与树。 文件系统是内容载体,AGENTS.md 是递归索引入口;CLI 执行明确的操作,不替调用方判断内容应该属于本层还是下层。
本目录的 pull request 由 Cursor Bugbot 按 .cursor/BUGBOT.md 自动审,并需 @VirusPC 人审(.github/CODEOWNERS)。
快速开始
运行、构建与测试以 Node.js 22.20 及以上为基线(与依赖 [email protected] 的 engines.node 一致)。以下命令在仓库根执行:
pnpm install --frozen-lockfile
pnpm --filter @viruspc/edges exec tsx src/index.ts --help
# 构建本地可执行产物
pnpm --filter @viruspc/edges build
node packages/cli/dist/index.js --help开发时可以通过 tsx 直接运行源码;需要 Schema 的命令先运行 build:schemas,需要审阅页的命令先构建审阅应用。pnpm --filter @viruspc/edges dev --help 会先生成 Schema。
公开包名是 @viruspc/edges,命令名只有 edges。另一台装了 Node.js 22.20 及以上的机器上:
npm install -g @viruspc/edges
edges --helppackage.json 的 bin 指向 dist/index.js。files 还包含构建时从 packages/skills/ 拷来的 packages/skills/。prepack 会执行完整构建,把审阅页编好的 HTML/JS/CSS、Schema、Memory 模板和这些 skill 放进安装包。tasks-review-app 只在本仓构建时使用,不单独发布;安装包里没有它的 React 源码,也没有 workspace: 依赖。不要用 npm publish 或 npm pack:npm 不会去掉 workspace 协议。本仓打本地安装包仍是 pnpm --filter @viruspc/edges pack。只有 build 或 prepack 成功后,编译后的命令才可用。
直接运行 edges 而不带子命令会返回用法错误;没有 edges-note 别名,也没有根命令默认写入笔记的行为。
发布到 npm
CLI 运行时只读 packages/cli/package.json 的 version。发版只升这一处,并写入 CHANGELOG.md。
版本合进 main 之后,在该提交上打 annotated tag @viruspc/edges@<version>(例如 @viruspc/[email protected])。发版 tag 必须是 annotated tag(git tag -a),轻量 tag 不会通过。推送这个 tag 会跑 .github/workflows/publish-edges-cli.yml:先测试,再经 GitHub Environment npm-publish 人工放行,用 npm Trusted Publishing(OIDC)发布(publishConfig.access 为 public)。version 不含 - 时执行 pnpm --filter @viruspc/edges publish --no-git-checks --access public,不传 --tag,默认 dist-tag 是 latest。version 含 -(semver prerelease)时同一命令加上 --tag beta,不把该版本标成 latest。Node 22.20 自带 npm 10.9.3,publish job 在 pnpm install 之前执行 npm install -g npm@^11.5.1,因为 Trusted Publishing 需要 npm >= 11.5.1,而 pnpm publish 会交给 npm 上传。不建 GitHub Release。决定见 ADR 0037。
合并后做一次(npm 上的 Trusted Publisher 若两天内没有成功发布会过期):
- npmjs.com → 包
@viruspc/edges→ Trusted Publisher:仓库VirusPC/edges,workflow 文件名publish-edges-cli.yml,environmentnpm-publish。 - GitHub → Environments → 新建
npm-publish,Required reviewers 设为 peng cheng。
整体架构
src/
├── index.ts # 进程入口
├── program.ts # Commander 命令树与 run()
├── context.ts # 单次命令的环境、stdin 和结果上下文
├── commands/ # 参数、输入输出与命令适配
├── packages/services/ # 完整用例、跨节点协调和持久化
├── domain/
│ ├── core/ # BaseNode、InternalNode、LeafNode、通用块机制
│ ├── models/ # 各节点类型、具体文法与目录分类
│ └── operations/ # 树遍历、惰性查询和集合算法
└── utils/ # Markdown、文件系统等基础工具flowchart TD
C[Commands:接收参数与呈现结果] --> S[Services:完成业务用例]
S --> M[Models:单节点规则]
S --> O[Operations:树与集合]
O --> M
M --> Core[Core:节点基类与通用块机制]
O --> Core
M --> U[通用 Markdown 工具]
S --> F[文件 IO、快照与写入协调]
S -. 提供加载回调 .-> O| 层 | 负责什么 | 例子 |
| --- | --- | --- |
| commands | 命令注册、参数与 stdin 适配、stdout/stderr 和退出码 | 把 notes create 转成 packages/services/notes/service 的创建请求 |
| services | 加载、跨节点协调、文件操作与完整业务流程 | 各领域 service.ts;创建 Task 后登记父 AGENTS、保存、移动整个任务目录 |
| core | 节点基类、本层/下层索引、关系和通用块机制 | BaseNode、InternalNode.addChild、core/node-content 按传入文法解析区块 |
| config | 入口文件名、叶子目录规则、区块标题与标记 | config/layout.ts 的 AGENTS.md、README.md、SKILL.md、INDEX.md 和 memory、task、note、project 规则;config/sections.ts 的 project-harness / project-entries 标题与标记 |
| models | 节点自身的字段、校验、解析序列化和索引编辑 | Task 优先级校验;models/node/layout.ts 按 memory、task、note、project 判定目录类型;AgentsNode 与 ReadmeNode 经 models/node/document.ts 使用 core/node-content |
| operations | 对多个元素进行遍历、筛选、分组、查找 | traverse、filter、groupBy、Task 数组排序 |
| utils | 可复用的底层格式与文件机制 | gray-matter 适配、路径与文件原语 |
Model 的 create/update/destroy 是内存领域方法;创建目录、保存文档等完整动作通过 Service 完成。operations 通过回调取得加载能力,不反向依赖 Service。不要让业务调用方重新拼装模型修改与文件读写。
每个领域模块有 packages/services/<module>/service.ts。commands 只从这份主文件进入,不直接装配 NodeService,也不直接 import 模块里的其他文件。实现可以留在原文件,主文件 re-export 即可,不必为了统一入口把函数搬一遍。memory 的 migrateMemory 在主文件里动态 import("./migrate.js"):这份实现可以不随 CLI 启动加载,命令入口仍是 service.ts。notes 与 projects 的主文件固定各自的叶子规格,再委托共用底层。
init 的主文件是 packages/services/init/service.ts。edges init 只从这份文件进入,做公共的系统入口步骤,再依次调用各模块自己的那一段。edges notes init、edges projects init、edges skills init、edges tasks init 经各自 service.ts 上的薄转发,只把本模块交给 init service。edges memory init 从 packages/services/memory/service.ts 的 initMemory 进入,只初始化 memory。模块之间不创建对方的材料。edges artifacts init 仍只写 artifacts 自己的 token 配置。
这些是跨领域工具,不另造 service.ts:packages/services/node/(node-service.ts、scope-session.ts,以及带日期 INDEX.md 叶子的 dated-leaf.ts)、scope.ts、list-query.ts、metadata.ts、config.ts、import-entry.ts。命令要用其中的符号时,由该领域的 service.ts 再导出。进程入口 program.ts 在分发命令前直接取 packages/services/node/node-lock.ts 的写锁,这不是某个领域命令。
notes、projects、skills、memory、tasks、artifacts、forest 的 commands 都只调用各自的 service.ts。tasks list 的看板目录名、README 位置与是否存在由 packages/services/tasks/service.ts 回答,命令不读挂载表。审阅页仍负责读输入、选输出路径和写 HTML。artifacts 的 token 配置写入仍在 command 侧。调用点收口了,list 的遍历顺序没有改写。
解耦 CLI commands 与 Service 记录了这层入口约定。packages/services/tasks/result.ts 仍组装命令运行时(地点、读写和时钟),审阅页命令仍负责读输入、选输出路径和写 HTML。
详细设计按职责分开阅读:
- Domain:整体依赖、写入与查询路径、Schema 链路。
- Models:类图、入口与目录、parent/children/harness、解析与扩展。
- Operations:函数与查询链、求值时机、短路和加载边界。
作用域与递归节点
先确定内容归属
edges --scope /absolute/project tasks list
edges --scope /absolute/project memory doctor作用域按 --scope → EDGES_SCOPE → EDGES_REPO → 当前工作目录附近的所属 AGENTS 作用域或 Git 根解析。相对路径基于调用时的 cwd,CLI 的安装目录不是默认内容目录;指定作用域不会自动初始化 Project Memory。
笔记写入选定作用域的 notes/,Git 操作使用它实际所属的仓库根。Artifacts 服务安装操作使用 CLI 实现仓库,独立于内容作用域。new-note MCP 未显式配置目标时保存调用方 cwd;显式配置优先,Skill 和 CLI 实现路径仍指向实现仓库。
目录是内容单位,入口是节点身份
所有内容节点采用目录形式:普通内容以 index.md 为入口,Skill 以 SKILL.md 为入口,内部索引以 AGENTS.md 为入口。ID 是规范化的绝对入口路径;图片和附件属于内容目录,不另建资源节点。
AGENTS 的本层硬约束、本层记忆、下层索引分别对应 constraints、localChildren、descendantChildren。NodeReference 只有 id,以及可选的 name、description;作者写下的相对链接由解析和序列化层维护。
每个节点可以有独立的 harness。内容叶节点的 harness 是同目录 AGENTS.md;AgentsNode 的下一层 harness 位于 .harness/AGENTS.md。harness 不混入 children,普通查询不会自动进入维护系统的下一层。
新增父级登记跟着主体系统走。路径在该系统的 .harness 里进本层;另一套系统入口进下层。已有关系保留原分组。缺少 owner 时不会自动初始化;生成的 Task 项目与 Memory 类型入口使用其固定本层系统维护信息关系。
查询遵循索引
NodeService.query 按已登记关系遍历,不靠扫描补齐遗漏。默认走全部组成 children(local∪descendants),含 .harness/compose/README.md;显式 localOnly 走 localChildren;根级 --exclude 按模块名跳过;includeHarness 才沿维护关系递归。traverse 一次走一个系统的 children。Compose 子节点是被组合仓库的 README.md,并且只到这一层。 多个系统由 Service 层收集并拼成森林,见多个系统拼成森林。默认根是 scope 下真 AGENTS.md。显式 --super 在这个 scope 的上一级建 SuperAgentsNode:当前 scope 目录就是该超节点的 .harness,材料 README 按 harness-materials.json 的 path 直接挂在 scope 下(tasks/README.md 即 <scope>/tasks/README.md)。scope 自己的 <scope>/.harness/ 是再下一层维护系统,由真 AGENTS.md 进入,超节点不把它再当成材料根。遍历仍当普通 AgentsNode,只走 children,不从真 AGENTS 并 README 边。详见 models 设计原则与 operations 遍历原则。
多个系统拼成森林
多个系统由 Service 层收集并拼成森林。
packages/services/node/system-roots.ts 的 collectSystemRoots 扫盘,收下带 project-harness 标记的 AGENTS.md。一份路径和正文算不算这种入口,由 models/agents/harness-agents.ts 的 isProjectHarnessAgentsFile 判断;这个函数只看传入的文本。
packages/services/node/system-forest-service.ts 的 buildSystemForest 对每个根各自做一次 traverse,得到 BaseNode[][]。independent(默认)在 resolve 遇到其它根时停住;innermost 在外层能走到内层时只留下内层。命令行是 edges forest list。
超节点的 children 只有这一层本层挂载,descendantChildren 为空。表里的 path 相对当前 scope;文件不存在就跳过,不扫盘补。以本仓库根为 --scope 时,九条材料里只有 tasks/README.md 和 README.md 在根上,所以 children 就是这两份。memory/、packages/skills/、evaluation/、observation/ 实际在 .harness/ 下,根上没有同名路径,不会挂上。.harness/tasks/README.md 也不是这个超节点的孩子。
edges --scope <仓库根> --super
虚拟超节点
└── <仓库根>/ ← 超节点的 .harness
├── tasks/README.md ← children
├── README.md ← children
└── .harness/ ← 仓库自己的维护系统,不在 children 里
├── tasks/README.md
└── memory/feedbacks/README.md把 --scope 指到 <仓库根>/.harness 才是下一层:这个目录本身成为超节点的 .harness。它的 children 是该目录下存在的 tasks/README.md、memory/feedbacks/README.md、memory/projects/README.md、memory/references/README.md、skills/managed/README.md、skills/referenced/README.md、projects/README.md、notes/README.md。这不是仓库根那一份名单。评测与观测在 packages/evaluation/、packages/observation/,不再是 harness 材料。
默认 list 从真 AGENTS.md 做一次 traverse。--all 从当前 --scope 走森林。--super 只换根。最全的一次查询是 --scope <仓库根> --super --all。这三个开关都在根命令上。
再往下走的是这两个 README 自己的 project-entries-*,不是超节点的另一组孩子。--super tasks list 只留下这条链上的 Task。
const pending = service.query(scope, { types: ["task"] })
.filter((node): node is TaskNode => node instanceof TaskNode)
.filter(task => task.priority === "high")
.groupBy(task => task.status)
.mapValues(tasks => tasks.map(task => task.title));
const groups = await pending.value();查询链只描述计算,value 才执行;重复求值会重新运行。普通 filter 不剪枝;类型选择由 Service 协助跳过无关叶节点正文。groupBy 虽然延迟执行,计算时仍需收集上游数据。
创建、保存与一致性
同一个 NodeService 内,同路径共享一个可变实例。update 保存节点的完整当前状态;create/import/move/destroy 会保存其实际影响到的索引的完整当前状态,包括这些索引已有的未保存修改,不顺便保存无关 dirty 节点。不同 Service 保持独立快照,不做三方合并。
query 最初只加载入口快照。对查询结果执行目录 move/destroy 前,用 get(node.path) 补充资源快照;它复用同一实例,不丢弃已有修改。list 返回的节点已捕获相应快照。目录移动与删除按完整生命周期单位处理,附件随目录操作。
节点写命令在业务读取前取得工作树级锁,结束后释放。父子作用域共享最近 Git worktree 根的 .edges-write.lock/,不同 worktree 独立;非 Git 写操作保守地共享系统临时目录中的同名锁。只读命令和 Artifacts 操作不取得这把节点锁;直接使用 NodeService 的调用方需要自行协调命令级锁。
锁由 proper-lockfile 管理,竞争时立即报 busy;.edges-write.lock 是保留运行时名称,扫描、复制会排除它,生命周期操作拒绝搬移或删除活动锁。其他仓库应自行配置对应忽略项,CLI 不代改其 .gitignore。
已有文件通过 write-file-atomic 替换,快照同时检查内容和文件身份。外部编辑器不遵守协作锁,因此仍存在检查到替换间的竞争窗口;单文件原子写入不等于多文件事务。失败时保留恢复路径和报告,不宣称崩溃下的全事务保证。
模型与文档处理保留未修改的业务扩展字段及 AGENTS 非受控正文。YAML 使用 gray-matter 的正常行为,不承诺编辑后保留 YAML 注释与样式。只读引用和管理根边界由 Service 执行,不把资源管理或新的业务权限体系塞进 Model。
命令
commands 的目录对应命令树:一个文件注册一个命令节点,同名子目录放它的子命令及参数、输出辅助代码。完整参数以各层 --help 为准。
任务:tasks
主体系统由 --scope 决定。一般任务写在 <scope>/.harness/tasks。--super 把主体换成该 scope 上的虚拟系统一,它的 harness 就是这个 scope 目录,任务写在 <scope>/tasks。--all 从当前 scope 走森林。最全的一次查询是 --scope <仓库根> --super --all。
edges --scope <directory> tasks list
edges --scope <directory> --all tasks list --status todo --priority high --sort priority
edges --scope <directory> --all tasks list --group-by project
edges --scope <directory> --super tasks list
edges --scope <directory> tasks create --title "修复构建" --project default
edges tasks get <stem或路径>
edges tasks update <stem或路径> --priority high
edges tasks status <stem或路径> done
edges tasks project list
edges tasks project review-page --from /tmp/tasks.json --out /tmp/review.html任务写操作只改文件,不执行 Git;取消使用 status cancelled,没有 delete 命令。任务以 <stem>/index.md 存储,并带 .<stem>.log.md;状态或项目变化移动整个目录。
edges --scope <目录> --super tasks list 在该 scope 上建超节点,列出从材料 README 能走到的任务,范围不改去 Git 根。edges --scope <目录> --all tasks list 从该 scope 走森林,不另跳 Git 根。未登记的看板不会因物理存在而自动出现。
普通任务命令 stdout 为 JSON;runs、run-messages 只读,默认表格,可用 --output json。未分组 list 返回 tasks 数组;分组输出是 { groupBy, groups: [{ key, items }] },先筛选再分组。条目携带 path、project、stem。
review-page 只把 groups/items JSON 渲染成 HTML,不改任务、不打开浏览器、不自动发布。需要公开预览时再调用 artifacts publish。固定任务站点的生成、部署与路径配置见部署说明。
项目记忆:memory
Project Memory 的执行能力由 TS CLI 提供,Skill 负责工作流和调用规范。
edges --scope /absolute/project init -- -g -y
edges --scope /absolute/project init notes
edges --scope /absolute/project notes init
edges --scope /absolute/project memory init --memory-types project feedback
edges --scope /absolute/project skills init --skill-types managed referenced
edges --scope /absolute/project skills init -- -g -y --agent claude-code
edges --scope /absolute/project init -- -y --agent '*'
edges --scope /absolute/project tasks init
edges --scope /absolute/project memory remember --type project --slug decision \
--description "记录本项目的设计取舍" --content-file /tmp/decision.md
edges --scope /absolute/project memory doctor
edges --scope /absolute/project memory doctor --apply
edges --scope /absolute/legacy-project memory migrate --recursive --dry-run
edges memory backup --repo-dir /absolute/project
edges memory restore --repo-dir /absolute/project --archive /private/archive.tar.gz新作用域的 edges memory init 未选择类型时只返回推荐项,不创建 notes、projects、skills 或 tasks 的材料。无参 edges init 的默认集包含 skills。edges skills init 既写 skill 类型索引,也把包内 skill 装给本机 agent:安装步骤是固定的 [email protected],只额外传入包内 skills 目录和 --skill '*',-- 之后的 token 原样转交。[email protected] 在探测到自己跑在 agent 里时会非交互,并只装给那个 agent;没有探测到、stdin 又不是 TTY 时,不带 -y 会退出 1。非交互且保留自动探测时用 edges init -- -g -y(-g 装到用户目录,-y 跳过确认;本机没有已安装 agent 时它会装给全部 agent)。指定某一个则用 edges init -- -y --agent claude-code(不加 -g 时写当前项目的 .claude/skills)。-- 只在 init 和 skills init 上切开;其他命令把 -- 和后面的 token 原样交给 commander。解析后的模块集合里没有 skills 时,这次 init 不会装,stderr 会列出被忽略的 token。memory 还在等类型选择、因而提前返回时,不因为没装 skills 而报这句。安装失败时进程退出码就是安装器的退出码,stdout 信封是 { status: "failed", errorCode: "SKILLS_INSTALL_FAILED", reason, created }。created 是失败前已经写好的相对路径。errorCode 与 VALIDATION_ERROR 不同;不要用退出码是不是 2 来判断是不是校验错误,安装器自己也可能退出 2。校验错误仍然是退出码 2、errorCode 为 VALIDATION_ERROR。edges tasks init 只写任务看板。remember 不初始化缺失作用域;doctor 默认只诊断。旧 .memory 通过显式 edges memory migrate 转换,普通命令不兼容迁移。edges init 遇到旧层会失败,并提示先跑 edges memory migrate --dry-run,看完再跑不带 --dry-run 的同一条命令。迁移认识目录式旧布局,也认识早期平铺的大写类型索引(FEEDBACK.md、PROJECT.md、REFERENCE.md、USER.md)和根上的 <type>_<slug>.md;被条目用相对路径引用的附件目录跟该条目进 <type>_<slug>/。无法归类的路径写在 JSON 的 diagnostics(unclassified-legacy-path)里,正式迁移时挪到 .project-memory-migration/unclassified/,不让一个文件挡住其余条目。符号链接和路径冲突仍然整次停止。新建子作用域按路径登记到父级:AGENTS.md 在父级 .harness 下记入本层,否则记入下层。已经登记的保持原组。edges memory doctor --apply 按同一规则补上缺失的父级链接。
用户记忆 restore 默认拒绝覆盖,明确使用 --force 才整份替换,失败时回滚。备份和恢复接受未压缩或 gzip tar,解包上限 256 MiB、10,000 文件,拒绝硬链接;中断后若报告 .private-user-memory-*/previous,应保留恢复副本。模板随包复制到 dist,运行不依赖相邻 Skill 源码或 Python。
内容类型、私有忽略规则与规范以 Project Memory LAYOUT 为准。
笔记:notes
edges --scope /absolute/project notes create --title "Design note" --body "正文。"
edges --scope /absolute/project notes get notes/2026-10-08--design-note/INDEX.md
edges --scope /absolute/project notes update notes/2026-10-08--design-note/INDEX.md --body "更新后的正文。"
edges --scope /absolute/project notes delete notes/2026-10-08--design-note/INDEX.mdnotes create 只在该 scope 的 notes/ 下本地创建叶子,不 commit、不 push、不开 PR。标题来自 --title 或正文里的一级标题,正文用 --body。没有 --content、--import-entry、--co-author、--mode、--dry-run 或 token 旗标。get 与 delete 只收目标路径;list 用共享的 --filter / --group-by。stdout 为 JSON,诊断在 stderr;退出码为成功 0、运行错误 1、用法或校验 2。
项目:projects
edges --scope /absolute/project projects create --title "Demo" --body "What this project is."
edges --scope /absolute/project projects list --filter title=Demo
edges --scope /absolute/project projects get projects/2026-10-08--demo/INDEX.mdprojects create 只在该 scope 的 projects/ 下本地创建叶子,不 commit、不 push、不开 PR。旗标与 notes 相同:metadata、--body,标题来自 --title 或正文一级标题。get 与 delete 只收目标;list 用共享的 --filter / --group-by。
技能:skills
edges --scope <directory> skills list
edges --scope <directory> skills get <name-or-path>
edges skills delete <name-or-path>skills create 与 skills update 写入当前 scope 的 skills/managed/<name>/SKILL.md(真系统在 <scope>/.harness/ 下)。edges memory remember 仍可另写 skill 类记忆,两者并列。没有 edges skill 别名。
例程:routines
edges --scope /absolute/project routines init
edges --scope /absolute/project routines create daily-review --description "每日复盘" --body "# 日复盘"
edges --scope /absolute/project routines list
edges --scope /absolute/project routines get daily-review
edges --scope /absolute/project routines update daily-review --body "更新后的日程与 Skill 引用"
edges --scope /absolute/project routines delete daily-review
edges --scope /absolute/project --super routines list
edges --scope /absolute/project --all routines listroutines init(或 init routines)显式采用模块,无参 init 的默认集不变。记录位于 <scope>/.harness/routines/<name>/INDEX.md,--super 创建与查询 <scope>/routines/;init 与其他模块一样只初始化真实 scope,不读 --super。名称为稳定的小写 kebab-case(1–64 字符),不加日期或 Project 分组。
create 需要名称和非空 --description,可带 --body 与重复的 --metadata key=value;update 修改说明、正文或 metadata。get/update/delete 接受名称或准确的入口路径,同名不唯一就报错。list 使用通用 --filter / --group-by;--all 只读,--exclude routines 排除模块。索引随 NodeService 的写入更新;查询沿已登记关系读取,不扫描目录补录孤立文件。
INDEX.md 用通用 name/description 元数据与 Markdown 正文,记录平台日程并链接同目录或外置 Skill。CLI 不解释 cron,不统一不同平台的日程格式。删除 routine 会处理整个目录,包括同目录 Skill;引用的外置 Skill 保留原处。
平台接入约定是:仓库作为配置依据;每次运行前读取记录和 Skill;默认读取来源仓库默认分支最新内容,明确指定的 branch/tag/commit 优先;读取失败停止本次并报告。修改日程后须另行更新平台配置。Edges 提供记录管理,不提供调度器、启停、缓存或平台连接器。完整示例见 edges-routines。
预览发布:artifacts
edges artifacts init --base-url https://edges.viruspc.tech
edges artifacts publish /tmp/review.html --ttl 24h
edges artifacts delete <id或URL>
edges artifacts server install
edges artifacts server start
edges artifacts server statusinit 管理客户端配置,publish/delete 调用预览服务并返回 JSON。server install 负责构建、服务配置和启用,不自动 start;start/stop/restart 只管理进程。服务部署路径与内容作用域独立。服务端读 ~/.config/edges/edges.env,不读 artifacts-preview.env。发布任务来源时使用 from-type、from-id、task-project,不把渲染审阅页与发布合成一个隐含动作。
客户端 token、服务端配置、nginx、升级与恢复流程见 Artifacts 服务说明和部署说明。
评测服务:evaluation
服务代码在本仓 packages/services/evaluation,不在 npm 安装包里。在克隆下来的本仓里,用这里构建出的 edges(pnpm --filter @viruspc/edges exec -- edges)。全局 npm install -g 的那份用来记笔记和装技能。
第一次要它留在这台机器上、一直接着实验:先 edges config init,再 install(只登记,不启动),再用不带 --dev 的 start。只想看着这次输出、结束进程就停,用 start --dev,它在前台跑。配置是 ~/.config/edges/edges.env。
edges config init
edges evaluation server install
edges evaluation server start
edges evaluation server status
edges evaluation server start --dev统一配置:config
edges config init
edges config path
edges config showinit 建立 ~/.config/edges/edges.env。旧的 artifacts-preview.env 只在新文件还不存在时被复制一次。path 打印路径。show 把密钥打成 ***。
组合仓库:compose
作用域 AGENTS.md 的 project-harness-compose 区块登记被组合的 harness 仓库。每条是一个本地目录链接:链接文字是名字,破折号后面是说明,remote: <git-url> 是 git 地址。完整操作见 edges-compose。
底层是 git submodule,CLI 只有三个子命令,名字对应 git submodule:add、list、update。没有 register、unregister、clone、pull、push、sync,也没有别名;推送和去掉条目交给原生 git 和手改 AGENTS.md(见下文)。
add 不传 --path 时,目录是仓内的 .harness/compose/<name>。区块里的链接永远是相对路径,相对于这份 AGENTS.md 所在的目录(在仓库根的 AGENTS.md 就是相对于 git 仓库根)。--path 只收相对路径,原样(规范化后)写进区块;绝对路径和 ~ 开头的路径以校验错误拒绝(退出码 2),不写区块。拒绝而不是只给 warning,是为了保证新写的区块换机器也能用。--description 必填,要写清这个仓存什么、什么时候该往里写;不传或传空字符串时校验失败,不写区块。
edges --scope <directory> compose add --name brain --remote https://github.com/example/brain.git --description "个人大脑:只在记跨项目判断时写入"
edges --scope <directory> compose add --name brain --path vendor/brain --remote https://github.com/example/brain.git --description "个人大脑:只在记跨项目判断时写入"
edges --scope <directory> compose list
edges --scope <directory> compose update
edges --scope <directory> compose update --remote --name brain| 命令 | 仓内条目(git submodule) | 旧条目 / 仓外条目 |
| --- | --- | --- |
| add --remote | git -C <仓库根> submodule add -- <remote> <path>,再写 .harness/compose/README.md。同一路径已是同一 URL 的子模块时只登记(JSON submodule: "existing"),URL 不同时报错。同名再 add 替换原来那一行。 | 跳过 submodule add,stderr 打 warning |
| list | 列出条目;每条带 href 和 initialized(目录存在且非空) | 同左 |
| update | git submodule update --init -- <path>,从不带 --recursive。条目还不在 .gitmodules 里时先 submodule add。 | 目录不存在时 git clone;已是同一 origin 的检出时不动 |
| update --remote | git submodule update --init --remote -- <path>。子模块在分支上时加 --merge,合进该分支,不切成 detached HEAD。 | 目录不存在时 git clone,否则 git pull |
update 的每条 action 是 initialized(原来没检出,现在有了)、present(已检出,没动)、updated(--remote 跟到了远端)或 failed。「仓内条目」指目录在 scope 所在 git 仓库的工作树里。scope 不在 git 仓库里、目录在仓库外(例如 --path ../notes),或区块里是旧的 ~/... / 绝对路径时,按旧条目处理。add 不带 --remote 时只登记本地目录,不碰 git。add 之后 .gitmodules 与子模块进了主仓库的暂存区,要和 .harness/compose/README.md 一起提交。
update 逐条处理:某一条失败(git 报错、URL 与登记不符、没有 remote 等)时,这条在 entries 里记为 action: "failed" 并带 error,stderr 打一行 warning: compose update failed for <名字>: <原因>,其余条目照常执行。至少一条成功时退出码 0;全部请求的条目都失败(含 --name 指定的那一条失败)时非零退出,失败结果里仍带 entries。缺少作用域 AGENTS.md、--name 找不到条目这类错误照旧整条命令失败。
推送:在子模块目录里用原生 git。子模块默认是 detached HEAD,先 git -C .harness/compose/<name> switch <branch>,提交后 git -C .harness/compose/<name> push。
去掉一条:在 .harness/compose/README.md 的 Compose 区块删掉那一行;要连子模块一起删时,跑 git submodule deinit -f <path> 和 git rm <path> 再提交。ComposeReadmeNode.removeChild 仍在,只是不再有 CLI 入口。
不递归:只看当前仓库自己的条目。update 不初始化被组合仓库里的子模块。默认查询会展开 compose/README.md,子节点是被组合仓库的 README.md,不展开该仓库自己的 Compose。系统森林扫描默认进入 scope 自己的 .harness/compose/,把其中带 project-harness 标记的 AGENTS.md 当作系统根;被组合仓库内部的 .harness/compose/ 不扫描。--exclude compose 同时跳过扫描和遍历。两个仓库互相登记也只到一层。被组合的仓库可以直接写,提交和推送用原生 git。
提示:条目目录不存在或是空的(子模块还没初始化)时,compose list 和默认会读到该条目的命令在 stderr 打 warning: compose entry <name> is not checked out at <href>; run edges compose update --name <name>。--exclude compose 时不打这行。旧的 ~/... / 绝对路径条目照样能读,并提示不带 --path 用同一个名字重新 compose add --remote ...,把旧行换成 <name>/README.md。
edges compose doctor 只做迁移:把还写在 AGENTS.md 上的区块搬进 .harness/compose/README.md,补上材料索引链接,改写 href。默认只打印会改什么,--apply 才写。其它文件里的 --with-composed 只报告、不改写;.agents/skills/、CHANGELOG 和 docs/adr/ 不列入。日常修改用 add / list / update。
根级 --exclude <modules> 与 --scope 同级,逗号分隔,可重复。允许 compose, memory, skills, notes, projects, tasks, routines。未知名字直接报错。与 --types 正交。没有 --only,也没有 --with-composed。
数据合同:schema
edges schema list
edges schema get task-doc/v1 > /tmp/task-doc.schema.json
pnpm --filter @viruspc/edges run build:schemaslist 返回已打包合同的 key/id/title/description,get 返回完整 draft-07 JSON Schema。它们不依赖内容作用域、不读 stdin,不在运行时生成 Schema;缺少构建产物应重新 build 或安装。成功 stdout 只有 JSON,错误写 stderr 并返回非零状态。
flowchart LR
T[TaskDoc 纯 TS 类型] --> R[schema-contracts.ts 登记]
R --> G[ts-json-schema-generator]
G --> D[dist/schemas:Schema 与 manifest]
D --> V[Ajv 输入校验]
D --> C[schema 命令]
T -. 类型引用 .-> UI[审阅前端]当前只有 TaskDoc 接入,类型源为 task-doc-contract.ts,不是整个 TaskNode 类。生成物不提交仓库。构建时生成,运行时 Ajv 校验;前端直接引用纯类型和枚举,不依赖生成器。
TaskDoc 要求 name、description、metadata、body,拒绝未知顶层字段;metadata 允许 JSON 扩展值,已知字段校验状态、优先级、项目标识与日期格式。校验不做类型强转、默认补值或字段删除。grouped-list 与 review-page 的 JSON 入口复用此合同;原始 Markdown 解析保留自身策略,不偷偷套用这套 JSON 校验。选型依据见 ADR 0025。
开发与验证
pnpm --filter @viruspc/edges exec tsc --noEmit
pnpm --filter @viruspc/edges test
pnpm --filter @viruspc/edges build
pnpm --filter @viruspc/edges run dev:tasks-review-apptest 会准备 Schema 和审阅页后执行 CLI 回归;build 清理 dist、生成 Schema、构建审阅应用、编译 TS 并复制 Memory 模板。包测试还会在仓库外仅安装生产依赖,验证编译产物可用。局部测试若直接运行,需自行准备对应资产。
新增行为按边界落位:单节点规则进 models,集合算法进 operations,完整用例进 services,命令协议进 commands。不要为一种新节点重复实现加载、索引、YAML 或文件写入,也不要为统一外观增加空层或无用包装。
旧 task-projects 区块需要独立迁移,普通任务写入不会自动转换。先预览选定范围,再明确执行:
pnpm --filter @viruspc/edges exec tsx ../../scripts/migrations/migrate-agents-indexes.mts \
--root /absolute/scope --check
pnpm --filter @viruspc/edges exec tsx ../../scripts/migrations/migrate-agents-indexes.mts \
--root /absolute/scope --write脚本先检查所有候选再写入,保留原文备份,跳过符号链接、依赖目录、受保护的 posts 和嵌套 Git 边界;重复执行无变更。
Task Project 列表属于看板 README.md(project-entries-local);看板 AGENTS.md 只登记 <project>/AGENTS.md 系统入口。把看板 AGENTS 里的 README 型 project 链接(含旧 task-projects 区块)一次性移过去:
pnpm migrate:task-project-lists -- --root /absolute/scope # 预览
pnpm migrate:task-project-lists -- --root /absolute/scope --apply
```目录迁移与本机私有内容恢复见[迁移指南](../../docs/recursive-layout-migration.md)。