npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 --help

package.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,environment npm-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.md

notes 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.md

projects 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 list

routines 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 status

init 管理客户端配置,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 show

init 建立 ~/.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:schemas

list 返回已打包合同的 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-app

test 会准备 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)。