上下文同步
把 Multica project 拉成本地文件树(clone / pull),把用过和产出的 context 推回 issue(push)。
ginit clone / ginit pull 把一个 Multica project 落成 <cwd>/.ginit 下的文件树——issue 正文、元数据、挂在上面的 context 和 agent session,子 issue 就是子目录;ginit push 反过来,把一条 context 引用挂到 context path 指向的那个 issue 上。装好之后,"读一条工作线"变成读本地目录,"收尾"变成一句 ginit push。
它不是 git,两个方向也不对称。 pull 的对象是一棵 project 子树,push 的对象是一条引用加一个 issue;push 不是把本地树的 diff 传回去。系统也不内置 merge——两份不一致的正文该怎么对齐,是干活的人或 agent 的判断,CLI 只负责把冲突摆到你面前、不覆盖。
前置条件
| 能力 | 要求 |
|---|---|
| clone / pull 落盘 | ginit ≥ v1.4.127 |
| add / diff 暂存区 | ginit ≥ v1.4.130 |
diff --staged 逐条比对远端 | ginit ≥ v1.4.131 |
push --type status 置 issue 状态 | ginit ≥ v1.4.133 |
| 全部命令 | 已 ginit multica login(只要 PAT,不需要注册 agent) |
版本看 ginit version,命令是否可用看它有没有自己的 usage(缺失会落回顶层 help):
ginit push --helpContext path:所有命令的寻址方式
/<workspace>/<project>[/<嵌套 project>…]/<ISSUE-KEY>[/<session>…]第一段是 workspace,必须小写。Multica 网页端的 issue 详情页可以一键复制这条路径,不用去翻 UUID。
三条容易踩的规则:
-
workspace 由路径第一段决定,所以带路径时不要再传
--workspace。 那个参数是断言不是选择器——指向别处会被直接拒绝;环境里的$MULTICA_WORKSPACE_ID会被路径覆盖,并在输出里说明。 -
pull 可以停在 project,push 不行。
/<workspace>/<project>对 pull 是"整个 project",对 push 是一个错误:refusing to push: "/silicon-brain/org-harness" addresses a project, and a push lands on an issue
写错的 workspace 不一定会报错。 各 workspace 的 issue 编号是独立的,KTB-305 写成 silicon-brain 下的路径会解析成 SII-305 并正常返回 2xx——推到了另一条工作线上,且没有任何告警。路径的第一段值得多看一眼。
拉下来:clone 与 pull
第一次用 clone,给 project id 或 project 名:
ginit clone --dir ~/work/org-harness --sessions none e3a0a5db-5a1a-4452-8b3d-84400b8a0aae参数要写在 project 名之前。 ginit clone <名字> --dir ... 这种写法会直接报 usage 错误退出。
之后在同一份副本里用 pull 更新。带 context path 就只落那一段子树:
ginit pull /silicon-brain/org-harness/SII-1517 --sessions nonepulled /root/work/org-harness/.ginit
project: org-harness (e3a0a5db-5a1a-4452-8b3d-84400b8a0aae)
workspace: silicon-brain (667aa432-f00b-4084-916a-3f38801fbd96) — from the context path's first segment
address: /silicon-brain/org-harness/SII-1517 (only this subtree is materialized)
issues: 1
sessions: 1 (1 个未取 — 详见 INDEX.md)
files: 21副本的根始终是被寻址的那个 project,寻址到 issue 只是决定这次物化哪一段。换一段就再 pull 一次,还是同一份副本,不会另起一个根。不带路径的 pull 按 config.json 里记着的范围刷新。
session 拉多少:三个档位
session 的转录可以很大,所以默认不拉正文。
--sessions | 拿到什么 |
|---|---|
meta(默认) | 只有 session 元数据 |
full | 连 session.jsonl 转录一起下载 |
none | 完全跳过 session |
副本会记住自己的档位,后续 pull 不会被默认值悄悄降级。要单独补某一条转录而不改变整体范围,用 --session <id>(可重复)。转录过大时 --max-session-bytes 会跳过它,默认 256 MiB。
没有权限的 session 会让整次 pull 失败,报 403 resource requires viewer。碰到就先 --sessions none 把树拉下来,再按需补单条。
本地长什么样
<cwd>/.ginit/
├── config.json # 这份副本对应哪个 workspace / project / 寻址范围 / session 档位
├── HEAD # 上次同步的内容摘要
├── INDEX.md # 人读的入口:issue 树、状态分布、这次没取到什么
├── project/
│ ├── meta.json
│ └── context/<id>.md # 挂在 project 层的 context
└── issues/<KEY>/
├── issue.md # 只有 description,没有 front matter
├── meta.json # 状态、负责人、父子、附件等全量元数据
├── context/
├── sessions/<context-item-id>/{meta.json, session.jsonl}
└── <子 KEY>/ # 子 issue 就是子目录两点值得记住:子 issue 的父子关系直接看路径,不需要读索引;issue.md 是唯一会和远端冲突的文件,因为只有它是正文,其余都是元数据。sessions/ 下只会有 session,不会冒出 issue 目录。
改了本地正文之后
pull 会先扫一遍受管文件,发现本地改过就整次拒绝执行,一个字节都不写:
ginit pull failed at pull/dirty-scan#/root/work/org-harness/.ginit: 1 managed file(s) differ from the last sync, so nothing was written:
issues/SII-1517/issue.md (modified)
Revert or move the rest and run `ginit pull` again.退出码是 4。解法有两个:改回去,或者把它 ginit add 起来——ginit 收下那份字节之后,刷新就能继续,你的改动留在暂存区里。
暂存与比对:add 与 diff
只推一条东西不需要暂存区,直接 ginit push 就行。要改 issue 正文、或者一次挂很多条,才走 add / diff / push。
ginit add 存的是"那一刻的字节",和 git 一样——之后再改工作树不影响已暂存的那份:
ginit add .ginit/issues/SII-1517/issue.md staged issues/SII-1517/issue.md (SII-1517, based on revision 0)
content:c4be95840b066927 content issues/SII-1517/issue.md (SII-1517)不带参数的 ginit diff 看"改了但还没暂存"的部分,纯本地;--staged 才去远端取当前正文现比,给出的正是 push 会写的内容:
ginit diff --stageddiff /root/work/org-harness/.ginit (staged vs remote — this is what `ginit push` would write)
issues/SII-1517/issue.md (SII-1517) +2 -0取不到远端时不会静默降级成本地比对;要显式降级成"只报文件名"用 --offline。
挂一条还不存在的 issue 用 --issue <handle> 先占个本地名字,push 时一起创建;--parent-handle 可以把这批新 issue 彼此嵌套起来。
推回去:push
ginit push <引用> <context-path> --type session|doc|wiki|url|comment|status--type 必填,命令不做形状推断——推断错了是静默绑错类型,代价由后来读这条 context 的人承担。
--type | 引用那一格填什么 | 行为 |
|---|---|---|
session | session URL 或 id | 上传本机转录并挂到 issue |
doc / wiki | 飞书文档 URL 或 token | 挂一条引用 |
url | 任意 URL | 挂一条引用 |
comment | 空串 "",正文走 --content | 追加一条评论 |
status | 状态本身,如 done | 设置 issue 状态 |
前五种都是追加,重跑幂等(按远端 source_ref 判等),两个人各推各的都会成功。status 是唯一"设置"而不是"追加"的那个,也不能进暂存区——所以习惯上先把 context 推完,状态放最后一条:
ginit push done /ktb/orgharness/ginit-pull-ginit-push/KTB-271 --type status --content-file deliverable.md几个用得上的参数:--role input|output 区分"这次用了它"和"这次产出了它";--title 给 context item 一个人读的标题;--parent-session 标明这条 session 从哪条分叉而来;--dry-run 把解析和判定都跑一遍但不写任何东西:
push /silicon-brain/org-harness/SII-1517
issue: SII-1517 (d106906f-d62e-4de0-91ca-f99ed45866a1)
would-push comment probe
would push: 1, already present: 0 (dry run — nothing was written)单条 push 不需要 .ginit。 目标由参数给全,在一个从没 clone 过的目录里也能用。只有回放模式才需要本地副本。
边干边记,收尾一次推完
干活途中顺手记,不在会话首尾拦人:
ginit context note <引用> --type doc --role input --title "评测口径讨论"收尾时一次推完:
ginit push --replay /ktb/orgharness/ginit-pull-ginit-push/KTB-271草稿只记引用不记正文,漏记一条不会让收尾卡住——只是少推那一条,结果里会说清推了几条。
退出码
| 码 | 含义 |
|---|---|
| 0 | 成功 |
| 1 | 其他失败(含 workspace 断言冲突) |
| 3 | clone 的目标已存在 .ginit,改用 pull |
| 4 | 本地受管文件已改,pull 拒绝执行 |
| 5 | push 的目标不是一个可写的 issue |
| 6 | 批量推送里有条目失败 |
| 7 | add 冲突 |
| 8 | diff --staged 取不到远端 |
常见问题
正文冲突了怎么办? 先 ginit pull 取回远端当前版本,自己把两份对齐,再 add + push。CLI 不会替你合并——这是设计取舍,不是缺失:上下文的合并是语义判断,做成三路合并只会得到一个看着对、读起来不对的正文。
diff --staged 说能推,push 却失败了? --staged 只验路径和条目内容,不验 workspace 那一侧的集成是否可用。典型的是飞书类型:没接飞书 bot 的 workspace(例如 KTB)推 --type doc / --type wiki 必然 409,而 --staged 对此无感知。
为什么我推上去的 session 是旧的? --type session 默认会重新上传本地转录。如果你要的就是"挂那次上传的快照、别再传一遍",用 --no-refresh;反过来,想让服务端拿到最新内容,就别加它。
--type local_path 还能用吗? 1.4.133 里还在,但它推的是路径不是内容,item 正文会是空的,已经在移除的路上。要把本地文件的内容带上去,用 --content-file。
ginit stash 和 ginit branch 呢? 这两个命令(把未推送的本地改动打包、一份 .ginit 并存多个 project)已经合进主干,但还没进 1.4.133 的发布——现在跑会落回顶层 help。等下一个版本。
评论
正在加载评论…