work-ally 0.2.0-alpha.1

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 (172) hide show
  1. package/AGENTS.md +110 -0
  2. package/DASHBOARD.md +160 -0
  3. package/PRODUCT.md +113 -0
  4. package/README.md +403 -0
  5. package/ally.sh +171 -0
  6. package/bridge/src/approval-rules.ts +360 -0
  7. package/bridge/src/channel-delivery.ts +207 -0
  8. package/bridge/src/channel-types.ts +22 -0
  9. package/bridge/src/channels/fake/adapter.ts +31 -0
  10. package/bridge/src/channels/feishu/adapter.ts +411 -0
  11. package/bridge/src/channels/feishu/approvals.ts +6 -0
  12. package/bridge/src/channels/feishu/formatter.ts +276 -0
  13. package/bridge/src/channels/feishu/normalize.ts +368 -0
  14. package/bridge/src/codex-config.ts +52 -0
  15. package/bridge/src/config.ts +240 -0
  16. package/bridge/src/fake-runtime-client.ts +505 -0
  17. package/bridge/src/handoff-service.ts +494 -0
  18. package/bridge/src/logger.ts +194 -0
  19. package/bridge/src/memory-digest.ts +186 -0
  20. package/bridge/src/receiver-approval-autonomy.ts +158 -0
  21. package/bridge/src/receiver-control-core.ts +140 -0
  22. package/bridge/src/receiver-control-work-session.ts +218 -0
  23. package/bridge/src/receiver-control.ts +83 -0
  24. package/bridge/src/receiver-delivery.ts +136 -0
  25. package/bridge/src/receiver-helpers.ts +96 -0
  26. package/bridge/src/receiver-human-gate.ts +333 -0
  27. package/bridge/src/receiver-inbound-preflight.ts +162 -0
  28. package/bridge/src/receiver-recovery.ts +236 -0
  29. package/bridge/src/receiver-runtime-callbacks.ts +367 -0
  30. package/bridge/src/receiver-runtime-policy.ts +132 -0
  31. package/bridge/src/receiver-runtime-state.ts +124 -0
  32. package/bridge/src/receiver-support-actions.ts +189 -0
  33. package/bridge/src/receiver-thread-start.ts +57 -0
  34. package/bridge/src/receiver-turn-coordination.ts +94 -0
  35. package/bridge/src/receiver-turn-execution.ts +257 -0
  36. package/bridge/src/receiver-turn-failure.ts +143 -0
  37. package/bridge/src/receiver-turn-result.ts +185 -0
  38. package/bridge/src/receiver-turn-steer.ts +70 -0
  39. package/bridge/src/receiver-work-session.ts +76 -0
  40. package/bridge/src/receiver.ts +329 -0
  41. package/bridge/src/router.ts +62 -0
  42. package/bridge/src/runtime-client-agent-messages.ts +150 -0
  43. package/bridge/src/runtime-client-message-dispatch.ts +176 -0
  44. package/bridge/src/runtime-client-protocol.ts +411 -0
  45. package/bridge/src/runtime-client-request-ops.ts +56 -0
  46. package/bridge/src/runtime-client-run-turn.ts +158 -0
  47. package/bridge/src/runtime-client-thread-ops.ts +270 -0
  48. package/bridge/src/runtime-client-transport.ts +309 -0
  49. package/bridge/src/runtime-client-turn-poll.ts +224 -0
  50. package/bridge/src/runtime-client-turn-read.ts +185 -0
  51. package/bridge/src/runtime-client-turn-state.ts +105 -0
  52. package/bridge/src/runtime-client.ts +344 -0
  53. package/bridge/src/runtime-user-input.ts +403 -0
  54. package/bridge/src/scheduler.ts +239 -0
  55. package/bridge/src/server-handoff-command.ts +364 -0
  56. package/bridge/src/server-main.ts +80 -0
  57. package/bridge/src/server-routine-command.ts +60 -0
  58. package/bridge/src/server-routine-execution.ts +222 -0
  59. package/bridge/src/server-runtime-app-support.ts +107 -0
  60. package/bridge/src/server-runtime-app.ts +238 -0
  61. package/bridge/src/server-thread-sync-command.ts +63 -0
  62. package/bridge/src/server.ts +17 -0
  63. package/bridge/src/session-store-delivery.ts +220 -0
  64. package/bridge/src/session-store-human-gate.ts +380 -0
  65. package/bridge/src/session-store-inbound-acceptance.ts +66 -0
  66. package/bridge/src/session-store-meta.ts +134 -0
  67. package/bridge/src/session-store-turn-ledger.ts +272 -0
  68. package/bridge/src/session-store.ts +380 -0
  69. package/bridge/src/system-notify.ts +220 -0
  70. package/bridge/src/thread-sync.ts +200 -0
  71. package/bridge/src/translator.ts +494 -0
  72. package/bridge/src/types.ts +289 -0
  73. package/bridge/src/utils.ts +104 -0
  74. package/bridge/src/work-session-store.ts +471 -0
  75. package/docs/.gitkeep +0 -0
  76. package/docs/architecture/codex-feishu-bridge-proposal.md +2742 -0
  77. package/docs/completed/FEATURE-feishu-markdown-and-reply-support.md +327 -0
  78. package/docs/completed/README.md +21 -0
  79. package/docs/completed/SPEC-approval-autonomy-and-safe-defaults.md +205 -0
  80. package/docs/completed/SPEC-approval-batch-and-strict-reply-shortcuts.md +153 -0
  81. package/docs/completed/SPEC-conversation-noise-reduction-and-busy-input-gate.md +538 -0
  82. package/docs/completed/SPEC-engineering-sop-skillization.md +190 -0
  83. package/docs/completed/SPEC-faithful-bridge-core-thinning-v2.md +376 -0
  84. package/docs/completed/SPEC-faithful-bridge-core-thinning.md +1071 -0
  85. package/docs/completed/SPEC-group-chat-sender-identity.md +301 -0
  86. package/docs/completed/SPEC-middleware-exception-visibility.md +227 -0
  87. package/docs/completed/SPEC-nightly-memory-digest-visibility.md +121 -0
  88. package/docs/completed/SPEC-project-group-chat-human-centered-conversation-mapping.md +326 -0
  89. package/docs/completed/SPEC-remove-cli-persona-bootstrap.md +201 -0
  90. package/docs/developer-workflow.md +49 -0
  91. package/docs/implementation/SPEC-codex-same-machine-session-handoff-implementation.md +239 -0
  92. package/docs/implementation/test-coverage-map.md +363 -0
  93. package/docs/implementation/work-ally-implementation-guide.md +790 -0
  94. package/docs/issues/README.md +10 -0
  95. package/docs/issues/pending/ANALYSIS-ally-premature-recovery-notice-and-task-state-semantics-2026-03-18.md +295 -0
  96. package/docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +466 -0
  97. package/docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +261 -0
  98. package/docs/issues/resolved/ANALYSIS-codex-app-server-transport-disconnect-semantics-2026-03-14.md +606 -0
  99. package/docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +348 -0
  100. package/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md +603 -0
  101. package/docs/issues/resolved/ANALYSIS-self-test-gap-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +166 -0
  102. package/docs/issues/resolved/ANALYSIS-self-test-gap-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +186 -0
  103. package/docs/issues/resolved/ANALYSIS-self-test-gap-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +166 -0
  104. package/docs/issues/resolved/REPORT-ally-runtime-turn-delivery-3b42fb8-2026-03-15.md +373 -0
  105. package/docs/manual-acceptance.md +127 -0
  106. package/docs/ops-runbook.md +44 -0
  107. package/docs/planning/FEATURE-memory-system.md +748 -0
  108. package/docs/planning/SPEC-active-turn-steer-and-context-compaction-visibility.md +269 -0
  109. package/docs/planning/SPEC-approval-rules-inheritance-and-local-validation-lane.md +450 -0
  110. package/docs/planning/SPEC-assistant-persona-bootstrap.md +199 -0
  111. package/docs/planning/SPEC-assistant-rename.md +610 -0
  112. package/docs/planning/SPEC-bridge-app-server-protocol-alignment.md +667 -0
  113. package/docs/planning/SPEC-claude-runtime-host-for-work-ally.md +434 -0
  114. package/docs/planning/SPEC-cli-feishu-codex-session-unification.md +236 -0
  115. package/docs/planning/SPEC-codex-same-machine-session-handoff.md +873 -0
  116. package/docs/planning/SPEC-feishu-reaction-shortcuts.md +282 -0
  117. package/docs/planning/SPEC-local-stable-release-boundary.md +166 -0
  118. package/docs/planning/SPEC-managed-thread-entry-and-surface-mobility.md +862 -0
  119. package/docs/planning/SPEC-minimal-bridge-semantics-and-user-visible-surface.md +362 -0
  120. package/docs/planning/SPEC-npm-alpha-distribution-and-install-first-release.md +222 -0
  121. package/docs/planning/SPEC-remove-websocket-runtime-transport.md +364 -0
  122. package/docs/planning/SPEC-runtime-abstraction-phase-1.md +424 -0
  123. package/docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md +274 -0
  124. package/docs/planning/SPEC-session-presence-and-state-visibility.md +397 -0
  125. package/docs/planning/SPEC-skill-first-capability-packaging.md +338 -0
  126. package/docs/planning/SPEC-stable-archive-contract.md +456 -0
  127. package/docs/planning/SPEC-supervised-start-boundary.md +127 -0
  128. package/docs/planning/SPEC-user-barrier-reduction-and-activation.md +832 -0
  129. package/docs/planning/ally-next.md +1278 -0
  130. package/docs/planning/assistant-workbench-spec.md +725 -0
  131. package/docs/planning/product-workbench.md +283 -0
  132. package/docs/product-onboarding.md +227 -0
  133. package/docs/product-spec-standard.md +528 -0
  134. package/docs/troubleshooting.md +45 -0
  135. package/docs/user-quickstart.md +46 -0
  136. package/internal/dispatch.sh +95 -0
  137. package/internal/lib/common.sh +1450 -0
  138. package/internal/modules/assistant/manage.sh +1312 -0
  139. package/internal/modules/bootstrap/setup.sh +144 -0
  140. package/internal/modules/config/init-env.sh +10 -0
  141. package/internal/modules/global/manage.sh +154 -0
  142. package/internal/modules/handoff/manage.sh +54 -0
  143. package/internal/modules/mcp/manage.sh +83 -0
  144. package/internal/modules/ops/logs.sh +76 -0
  145. package/internal/modules/routines/manage.sh +55 -0
  146. package/internal/modules/runtime/assistant-autosave.sh +26 -0
  147. package/internal/modules/runtime/restart.sh +6 -0
  148. package/internal/modules/runtime/start.sh +283 -0
  149. package/internal/modules/runtime/status.sh +194 -0
  150. package/internal/modules/runtime/stop.sh +55 -0
  151. package/internal/modules/runtime/supervisor.sh +216 -0
  152. package/internal/modules/runtime/update.sh +26 -0
  153. package/package.json +41 -0
  154. package/runtime/config/.gitkeep +0 -0
  155. package/runtime/host/.gitkeep +0 -0
  156. package/runtime/host/healthcheck-codex-app-server.ts +22 -0
  157. package/runtime/host/ping-pong-codex-app-server.ts +66 -0
  158. package/runtime/host/probe-codex-app-server.ts +115 -0
  159. package/skills/archive-reader/SKILL.md +9 -0
  160. package/skills/feishu-production-debug/SKILL.md +37 -0
  161. package/skills/feishu-production-debug/references/feishu-debug-order.md +49 -0
  162. package/skills/feishu-production-debug/references/platform-permission-baseline.md +23 -0
  163. package/skills/issue-to-spec-triage/SKILL.md +44 -0
  164. package/skills/issue-to-spec-triage/references/triage-rules.md +66 -0
  165. package/skills/memory-digest/SKILL.md +9 -0
  166. package/skills/post-implementation-closure/SKILL.md +39 -0
  167. package/skills/post-implementation-closure/references/closure-checklist.md +45 -0
  168. package/skills/post-implementation-closure/references/doc-drift-map.md +49 -0
  169. package/skills/product-spec/SKILL.md +244 -0
  170. package/templates/env.example +5 -0
  171. package/templates/routines/nightly-memory-digest.yaml +10 -0
  172. package/templates/workspace/AGENTS.md +26 -0
@@ -0,0 +1,1312 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5
+ # shellcheck disable=SC1091
6
+ . "$SCRIPT_DIR/../../lib/common.sh"
7
+ work_ally_init_context
8
+ work_ally_ensure_state_dirs
9
+
10
+ SUBCMD="${1:-list}"
11
+ shift || true
12
+
13
+ cmd_name=$(work_ally_cmd_name)
14
+
15
+ usage() {
16
+ cat <<USAGE
17
+ Usage: $cmd_name assistant <subcmd> [args]
18
+
19
+ Subcommands:
20
+ add <name> --workspace <path> [--description TEXT] [--git-remote URL]
21
+ ensure <name> --workspace <path> [--description TEXT] [--git-remote URL]
22
+ bind <name> --workspace <path>
23
+ remove <name>
24
+ rename <old> <new>
25
+ list
26
+ show <name>
27
+ USAGE
28
+ }
29
+
30
+ assistant_profile_agents_template() {
31
+ local assistant_home="$1"
32
+ local workspace_root="$2"
33
+ cat <<'AGENTS' | sed \
34
+ -e "s|__WORKSPACE_ROOT__|$workspace_root|g" \
35
+ -e "s|__ASSISTANT_HOME__|$assistant_home|g"
36
+ # AGENTS.md
37
+
38
+ ## 角色定位
39
+
40
+ 你是由 `work-ally` 启动的命名 assistant。
41
+
42
+ 你的 assistant 办公桌是你的长期住处。当前项目目录只是你此刻工作的现场。
43
+
44
+ ## 两个主规则入口
45
+
46
+ - 当前项目规则入口:__WORKSPACE_ROOT__/AGENTS.md
47
+ - assistant desk 规则入口:__ASSISTANT_HOME__/AGENTS.md
48
+
49
+ 如果项目 `AGENTS.md` 对角色、表达风格、协作方式有明确要求,应以项目要求为准。
50
+
51
+ ## 关键路径
52
+
53
+ - assistant desk 根目录:__ASSISTANT_HOME__
54
+ - 当前项目目录:__WORKSPACE_ROOT__
55
+ - Runtime Codex home:__ASSISTANT_HOME__/.system/codex-home
56
+
57
+ 不要把 desk 内的路径误解为相对于当前项目根目录的路径。它们都属于上面的 desk 路径。
58
+
59
+ ## 读取模型
60
+
61
+ 在 Codex 吃到两个 `AGENTS.md` 之后,你还必须继续读取这些 desk 资产:
62
+
63
+ 1. __ASSISTANT_HOME__/SOUL.md
64
+ 2. __ASSISTANT_HOME__/NOW.md
65
+ 3. __ASSISTANT_HOME__/MISTAKES.md
66
+ 4. __ASSISTANT_HOME__/MEMORY.md
67
+
68
+ 其中:
69
+
70
+ - `SOUL.md`:你的名字、使命、人格、表达风格、做事方式
71
+ - `NOW.md`:当前态便签,工作时优先看,也允许你随手更新
72
+ - `MISTAKES.md`:高价值错题本,遇到高风险场景前先留意
73
+ - `MEMORY.md`:长期稳定记忆,不写日常碎片
74
+
75
+ ## 边界
76
+
77
+ - 你的身份、记忆、对话视图、运行状态都在这个 desk 里。
78
+ - 项目的构建、测试、仓库规则来自当前项目目录。
79
+ - 不要把这个 desk 当成项目仓库。
80
+ - 稳定的项目事实应沉淀回项目自身,而不是长期堆在 desk 里。
81
+
82
+ ## Desk 资产
83
+
84
+ - __ASSISTANT_HOME__/SOUL.md
85
+ - __ASSISTANT_HOME__/NOW.md
86
+ - __ASSISTANT_HOME__/MISTAKES.md
87
+ - __ASSISTANT_HOME__/MEMORY.md
88
+ - __ASSISTANT_HOME__/journal/
89
+ - __ASSISTANT_HOME__/conversations/
90
+ - __ASSISTANT_HOME__/.system/
91
+
92
+ 补充说明:
93
+
94
+ - `conversations/` 是对话可读视图层,适合查看完整聊天链路。
95
+ - `.system/archive/` 是稳定原材料层,通常不需要主动阅读;只有在追溯事实、排查异常时才按需查阅。
96
+ - `SOUL.md` 允许用户自由编辑,但你默认不应静默改写它;如确需调整,应明确提出建议。
97
+ - nightly digest 默认只产出 `journal/` 与 `MEMORY.md`,不负责改写 `NOW.md`。
98
+ AGENTS
99
+ }
100
+
101
+ assistant_profile_soul_template() {
102
+ local name="$1"
103
+
104
+ cat <<'SOUL' | sed -e "s|__ASSISTANT_NAME__|$name|g"
105
+ # SOUL
106
+
107
+ ## 你的名称
108
+
109
+ - 名字:__ASSISTANT_NAME__
110
+
111
+ ## 用户补充设定
112
+
113
+ - 待使用者补充。
114
+
115
+ ## 核心身份
116
+
117
+ - 你是长期驻留在 assistant desk 的命名助理。
118
+ - 你的职责是把问题想清楚、推进到位,并沉淀成可复用结果。
119
+ - 你进入项目是来工作,不是来改写项目规则;项目现场优先遵守项目 `AGENTS.md`。
120
+
121
+ ## 使命
122
+
123
+ - 先判断问题本质,再推进到可执行结论。
124
+ - 帮使用者节省注意力:先说结果,再给关键依据,再给下一步。
125
+ - 把重要判断沉淀进记忆系统,而不是只留在一次对话里。
126
+ - 待使用者补充。
127
+
128
+ ## 性格与表达风格
129
+
130
+ - 默认使用中文。
131
+ - 亲切、知性、直接、不盲从。
132
+ - 说话像可信赖的搭档,不像客服,也不写官话。
133
+ - 语气温和,但观点明确;该提醒时直接提醒。
134
+ - 主战场按即时消息场景处理:短、准、够用。
135
+ - 可以少量使用表情,但必须克制。
136
+ - 待使用者补充。
137
+
138
+ ## IM 回复风格
139
+
140
+ - 先结论,后补充。
141
+ - 默认短回复;能一句话说清,就不要写一段。
142
+ - 只回答当前最需要的信息,不主动倾倒整包分析。
143
+ - 汇报优先短句和子弹点,方便几秒扫完。
144
+ - 非必要不展开推理过程;必须展开时再完整说明。
145
+ - 回复像给领导过目:亲切,但一针见血。
146
+
147
+ ## 做事方式
148
+
149
+ - 先把事情做实,再汇报结果。
150
+ - 先抓主干,再补细节;先判断边界,再处理实现。
151
+ - 发现缺口时主动补齐;发现风险时及时提醒。
152
+ - 重要信息写入合适文件,不依赖短暂上下文。
153
+ - 用户已明确授权的计划内事项,直接推进,不重复确认。
154
+ - 待使用者补充。
155
+
156
+ ## 禁忌与边界
157
+
158
+ - 不伪造事实,不装作知道。
159
+ - 不因为迎合而放弃独立判断。
160
+ - 不把项目规则覆盖成个人偏好。
161
+ - 不为显得完整而堆低价值噪音。
162
+ - 不写啰嗦的大段废话。
163
+ - 未经明确授权,不静默改写本文件。
164
+ - 待使用者补充。
165
+ SOUL
166
+ }
167
+
168
+ assistant_profile_global_config_path() {
169
+ printf '%s/.codex/config.toml\n' "$HOME"
170
+ }
171
+
172
+ assistant_profile_default_config_template() {
173
+ cat <<'CONFIG'
174
+ model = "gpt-5"
175
+ reasoning_effort = "medium"
176
+ verbosity = "medium"
177
+ CONFIG
178
+ }
179
+
180
+ assistant_profile_config_template() {
181
+ local global_config
182
+ global_config=$(assistant_profile_global_config_path)
183
+ if [ -f "$global_config" ]; then
184
+ awk '
185
+ BEGIN {
186
+ skip_projects = 0
187
+ }
188
+ /^\[projects\.[^]]+\][[:space:]]*$/ {
189
+ skip_projects = 1
190
+ next
191
+ }
192
+ /^\[/ {
193
+ skip_projects = 0
194
+ }
195
+ skip_projects {
196
+ next
197
+ }
198
+ {
199
+ print
200
+ }
201
+ ' "$global_config"
202
+ return 0
203
+ fi
204
+ assistant_profile_default_config_template
205
+ }
206
+
207
+ assistant_profile_ensure_trusted_project() {
208
+ local config_file="$1"
209
+ local project_root="$2"
210
+ [ -n "$project_root" ] || return 0
211
+ [ -f "$config_file" ] || return 0
212
+ if grep -Fq "[projects.\"$project_root\"]" "$config_file"; then
213
+ return 0
214
+ fi
215
+ printf '\n[projects."%s"]\ntrust_level = "trusted"\n' "$project_root" >> "$config_file"
216
+ }
217
+
218
+ assistant_profile_remove_trusted_project() {
219
+ local config_file="$1"
220
+ local project_root="$2"
221
+ local tmp_file
222
+ [ -f "$config_file" ] || return 0
223
+ [ -n "$project_root" ] || return 0
224
+
225
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/work-ally-codex-config.XXXXXX")
226
+ awk -v project_root="$project_root" '
227
+ BEGIN {
228
+ skip = 0
229
+ section_header = "[projects.\"" project_root "\"]"
230
+ }
231
+ $0 == section_header {
232
+ skip = 1
233
+ next
234
+ }
235
+ skip && /^\[/ {
236
+ skip = 0
237
+ print
238
+ next
239
+ }
240
+ skip {
241
+ next
242
+ }
243
+ {
244
+ print
245
+ }
246
+ ' "$config_file" > "$tmp_file"
247
+ mv "$tmp_file" "$config_file"
248
+ }
249
+
250
+ assistant_profile_set_top_level_setting() {
251
+ local config_file="$1"
252
+ local key="$2"
253
+ local value="$3"
254
+ local tmp_file
255
+ [ -f "$config_file" ] || return 0
256
+
257
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/work-ally-codex-config.XXXXXX")
258
+ awk -v key="$key" -v value="$value" '
259
+ BEGIN {
260
+ in_section = 0
261
+ replaced = 0
262
+ }
263
+ /^\[/ {
264
+ if (!in_section && !replaced) {
265
+ print key " = " value
266
+ print ""
267
+ replaced = 1
268
+ }
269
+ in_section = 1
270
+ print
271
+ next
272
+ }
273
+ {
274
+ if (!in_section && $0 ~ "^[[:space:]]*" key "[[:space:]]*=") {
275
+ if (!replaced) {
276
+ print key " = " value
277
+ replaced = 1
278
+ }
279
+ next
280
+ }
281
+ print
282
+ }
283
+ END {
284
+ if (!replaced) {
285
+ if (NR > 0) {
286
+ print ""
287
+ }
288
+ print key " = " value
289
+ }
290
+ }
291
+ ' "$config_file" > "$tmp_file"
292
+ mv "$tmp_file" "$config_file"
293
+ }
294
+
295
+ assistant_profile_set_workspace_write_root() {
296
+ local config_file="$1"
297
+ local assistant_home="$2"
298
+ local tmp_file
299
+ [ -f "$config_file" ] || return 0
300
+ [ -n "$assistant_home" ] || return 0
301
+
302
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/work-ally-codex-config.XXXXXX")
303
+ awk -v assistant_home="$assistant_home" '
304
+ BEGIN {
305
+ in_section = 0
306
+ section_found = 0
307
+ writable_written = 0
308
+ line_to_insert = "writable_roots = [\"" assistant_home "\"]"
309
+ }
310
+ function flush_if_needed() {
311
+ if (in_section && !writable_written) {
312
+ print line_to_insert
313
+ writable_written = 1
314
+ }
315
+ }
316
+ /^\[sandbox_workspace_write\][[:space:]]*$/ {
317
+ print
318
+ in_section = 1
319
+ section_found = 1
320
+ next
321
+ }
322
+ /^\[/ {
323
+ flush_if_needed()
324
+ in_section = 0
325
+ print
326
+ next
327
+ }
328
+ {
329
+ if (in_section && $0 ~ /^[[:space:]]*writable_roots[[:space:]]*=/) {
330
+ if (!writable_written) {
331
+ print line_to_insert
332
+ writable_written = 1
333
+ }
334
+ next
335
+ }
336
+ print
337
+ }
338
+ END {
339
+ flush_if_needed()
340
+ if (!section_found) {
341
+ print ""
342
+ print "[sandbox_workspace_write]"
343
+ print line_to_insert
344
+ }
345
+ }
346
+ ' "$config_file" > "$tmp_file"
347
+ mv "$tmp_file" "$config_file"
348
+ }
349
+
350
+ assistant_profile_ensure_runtime_defaults() {
351
+ local config_file="$1"
352
+ local assistant_home="$2"
353
+ assistant_profile_set_top_level_setting "$config_file" "sandbox_mode" '"workspace-write"'
354
+ assistant_profile_set_top_level_setting "$config_file" "approval_policy" '"on-request"'
355
+ assistant_profile_set_workspace_write_root "$config_file" "$assistant_home"
356
+ }
357
+
358
+ assistant_gitignore_template() {
359
+ cat <<'IGNORE'
360
+ .DS_Store
361
+ .system/config.env
362
+ .system/logs/
363
+ .system/cache/
364
+ .system/runtime/
365
+ .system/runs/
366
+ .system/codex-home/.personality_migration
367
+ .system/codex-home/logs_*.sqlite
368
+ .system/codex-home/logs_*.sqlite-shm
369
+ .system/codex-home/logs_*.sqlite-wal
370
+ .system/codex-home/state_*.sqlite
371
+ .system/codex-home/state_*.sqlite-shm
372
+ .system/codex-home/state_*.sqlite-wal
373
+ .system/codex-home/sessions/
374
+ .system/codex-home/shell_snapshots/
375
+ .system/codex-home/skills/
376
+ .system/codex-home/tmp/
377
+ IGNORE
378
+ }
379
+
380
+ assistant_gitignore_ensure_patterns() {
381
+ local assistant_home="$1"
382
+ local gitignore_file="$assistant_home/.gitignore"
383
+ local line
384
+
385
+ [ -f "$gitignore_file" ] || assistant_gitignore_template > "$gitignore_file"
386
+
387
+ while IFS= read -r line; do
388
+ [ -n "$line" ] || continue
389
+ if ! grep -Fqx "$line" "$gitignore_file"; then
390
+ printf '%s
391
+ ' "$line" >> "$gitignore_file"
392
+ fi
393
+ done <<'IGNORE'
394
+ .DS_Store
395
+ .system/config.env
396
+ .system/logs/
397
+ .system/cache/
398
+ .system/runtime/
399
+ .system/runs/
400
+ .system/codex-home/.personality_migration
401
+ .system/codex-home/logs_*.sqlite
402
+ .system/codex-home/logs_*.sqlite-shm
403
+ .system/codex-home/logs_*.sqlite-wal
404
+ .system/codex-home/state_*.sqlite
405
+ .system/codex-home/state_*.sqlite-shm
406
+ .system/codex-home/state_*.sqlite-wal
407
+ .system/codex-home/sessions/
408
+ .system/codex-home/shell_snapshots/
409
+ .system/codex-home/skills/
410
+ .system/codex-home/tmp/
411
+ IGNORE
412
+ }
413
+
414
+ build_registry_entry() {
415
+ local name="$1"
416
+ local assistant_home="$2"
417
+ local codex_home="$3"
418
+ local workspace_root="$4"
419
+ local description="$5"
420
+ local created_at_override="${6:-}"
421
+ local updated_at_override="${7:-}"
422
+ local timestamp created_at updated_at
423
+ timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
424
+ created_at="${created_at_override:-$timestamp}"
425
+ updated_at="${updated_at_override:-$timestamp}"
426
+ description=$(printf '%s' "$description" | sed 's/\\/\\\\/g; s/"/\\"/g')
427
+ cat <<ENTRY
428
+ $name:
429
+ assistant_home: $assistant_home
430
+ codex_home: $codex_home
431
+ workspace_root: $workspace_root
432
+ description: "$description"
433
+ created_at: $created_at
434
+ updated_at: $updated_at
435
+ ENTRY
436
+ }
437
+
438
+ bootstrap_assistant() {
439
+ local name="$1"
440
+ local workspace_root="$2"
441
+ local description="$3"
442
+ local git_remote="$4"
443
+ local existing_workspace="${5:-}"
444
+ local assistant_home system_dir codex_home conversations_dir sessions_dir archive_dir journal_dir routines_dir runtime_dir logs_dir runs_dir cache_dir
445
+
446
+ assistant_home=$(work_ally_assistant_home "$name")
447
+ system_dir=$(work_ally_assistant_system_dir "$name")
448
+ codex_home=$(work_ally_assistant_codex_home "$name")
449
+ conversations_dir=$(work_ally_assistant_conversations_dir "$name")
450
+ sessions_dir=$(work_ally_assistant_sessions_dir "$name")
451
+ archive_dir=$(work_ally_assistant_archive_dir "$name")
452
+ journal_dir=$(work_ally_assistant_journal_dir "$name")
453
+ routines_dir=$(work_ally_assistant_routines_dir "$name")
454
+ runtime_dir=$(work_ally_assistant_runtime_dir "$name")
455
+ logs_dir=$(work_ally_assistant_logs_dir "$name")
456
+ runs_dir=$(work_ally_assistant_runs_dir "$name")
457
+ cache_dir=$(work_ally_assistant_cache_dir "$name")
458
+
459
+ mkdir -p \
460
+ "$assistant_home" \
461
+ "$system_dir" \
462
+ "$codex_home" \
463
+ "$conversations_dir" \
464
+ "$sessions_dir" \
465
+ "$archive_dir" \
466
+ "$journal_dir" \
467
+ "$routines_dir" \
468
+ "$runtime_dir" \
469
+ "$logs_dir" \
470
+ "$runs_dir" \
471
+ "$cache_dir"
472
+
473
+ if [ ! -f "$assistant_home/AGENTS.md" ]; then
474
+ assistant_profile_agents_template "$assistant_home" "$workspace_root" > "$assistant_home/AGENTS.md"
475
+ fi
476
+ if [ ! -f "$codex_home/AGENTS.md" ]; then
477
+ assistant_profile_agents_template "$assistant_home" "$workspace_root" > "$codex_home/AGENTS.md"
478
+ fi
479
+ if [ ! -f "$assistant_home/SOUL.md" ]; then
480
+ assistant_profile_soul_template "$name" > "$assistant_home/SOUL.md"
481
+ fi
482
+ if [ ! -f "$codex_home/config.toml" ]; then
483
+ assistant_profile_config_template > "$codex_home/config.toml"
484
+ fi
485
+ assistant_profile_remove_trusted_project "$codex_home/config.toml" "$existing_workspace"
486
+ assistant_profile_ensure_trusted_project "$codex_home/config.toml" "$workspace_root"
487
+ assistant_profile_ensure_trusted_project "$codex_home/config.toml" "$assistant_home"
488
+ assistant_profile_ensure_runtime_defaults "$codex_home/config.toml" "$assistant_home"
489
+ assistant_gitignore_ensure_patterns "$assistant_home"
490
+ if [ ! -f "$assistant_home/NOW.md" ]; then
491
+ cat > "$assistant_home/NOW.md" <<'NOW'
492
+ # NOW
493
+
494
+ ## 当前在做什么
495
+ - 待更新
496
+
497
+ ## 当前阻塞
498
+ - 待更新
499
+
500
+ ## 下一步
501
+ - 待更新
502
+ NOW
503
+ fi
504
+ if [ ! -f "$assistant_home/MEMORY.md" ]; then
505
+ cat > "$assistant_home/MEMORY.md" <<'MEM'
506
+ # MEMORY
507
+
508
+ 暂无长期记忆。
509
+ MEM
510
+ fi
511
+ if [ ! -f "$assistant_home/MISTAKES.md" ]; then
512
+ cat > "$assistant_home/MISTAKES.md" <<'MISTAKES'
513
+ # MISTAKES
514
+
515
+ 暂无错题记录。
516
+ MISTAKES
517
+ fi
518
+ if [ ! -f "$journal_dir/README.md" ]; then
519
+ cat > "$journal_dir/README.md" <<'JOURNAL'
520
+ # Journal
521
+
522
+ 按天记录整理后的工作日记与沉淀。
523
+ JOURNAL
524
+ fi
525
+ if [ ! -f "$conversations_dir/README.md" ]; then
526
+ cat > "$conversations_dir/README.md" <<'CONVERSATIONS'
527
+ # Conversations
528
+
529
+ 这里保存从原材料派生出的对话可读视图,便于人和 assistant 回看完整聊天链路。
530
+
531
+ - 这里是 view layer,不是 source of truth。
532
+ - source of truth 在 `.system/archive/`。
533
+ CONVERSATIONS
534
+ fi
535
+ if [ ! -f "$archive_dir/README.md" ]; then
536
+ cat > "$archive_dir/README.md" <<'ARCHIVE'
537
+ # Archive
538
+
539
+ 稳定原材料层,按 append-only 方式记录完整事实流水。
540
+
541
+ - 这里不是长期记忆。
542
+ - 这里不是默认阅读界面。
543
+ - 只有在追溯事实、审计与排障时才按需查阅。
544
+ ARCHIVE
545
+ fi
546
+ if [ ! -f "$routines_dir/nightly-memory-digest.yaml" ]; then
547
+ cp "$WORK_ALLY_IMPLEMENTATION_DIR/templates/routines/nightly-memory-digest.yaml" "$routines_dir/nightly-memory-digest.yaml"
548
+ fi
549
+
550
+ work_ally_assistant_git_setup "$assistant_home" "$git_remote" || true
551
+ work_ally_assistant_git_checkpoint "$assistant_home" "bootstrap assistant $name" || true
552
+
553
+ printf '%s\n%s\n' "$assistant_home" "$codex_home"
554
+ }
555
+
556
+ ensure_registry_entry() {
557
+ local name="$1"
558
+ local assistant_home="$2"
559
+ local codex_home="$3"
560
+ local workspace_root="$4"
561
+ local description="$5"
562
+
563
+ if [ ! -f "$WORK_ALLY_ASSISTANT_REGISTRY_FILE" ]; then
564
+ printf 'assistants:\n' > "$WORK_ALLY_ASSISTANT_REGISTRY_FILE"
565
+ fi
566
+
567
+ if ! work_ally_registry_has_assistant "$name"; then
568
+ build_registry_entry "$name" "$assistant_home" "$codex_home" "$workspace_root" "$description" >> "$WORK_ALLY_ASSISTANT_REGISTRY_FILE"
569
+ fi
570
+ }
571
+
572
+ assistant_registry_update_binding() {
573
+ local name="$1"
574
+ local workspace_root="$2"
575
+ local tmp timestamp
576
+
577
+ [ -f "$WORK_ALLY_ASSISTANT_REGISTRY_FILE" ] || work_ally_die "Assistant registry not found: $WORK_ALLY_ASSISTANT_REGISTRY_FILE"
578
+
579
+ timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
580
+ tmp=$(mktemp "${TMPDIR:-/tmp}/work-ally-assistant-registry.XXXXXX")
581
+
582
+ awk -v assistant="$name" -v workspace="$workspace_root" -v updated_at="$timestamp" '
583
+ BEGIN {
584
+ in_block = 0
585
+ workspace_done = 0
586
+ updated_done = 0
587
+ }
588
+ function flush_missing() {
589
+ if (!in_block) {
590
+ return
591
+ }
592
+ if (!workspace_done) {
593
+ print " workspace_root: " workspace
594
+ }
595
+ if (!updated_done) {
596
+ print " updated_at: " updated_at
597
+ }
598
+ }
599
+ $0 ~ "^ " assistant ":$" {
600
+ in_block = 1
601
+ workspace_done = 0
602
+ updated_done = 0
603
+ print
604
+ next
605
+ }
606
+ in_block && /^ [^[:space:]]+:$/ {
607
+ flush_missing()
608
+ in_block = 0
609
+ print
610
+ next
611
+ }
612
+ in_block && /^ workspace_root: / {
613
+ print " workspace_root: " workspace
614
+ workspace_done = 1
615
+ next
616
+ }
617
+ in_block && /^ updated_at: / {
618
+ print " updated_at: " updated_at
619
+ updated_done = 1
620
+ next
621
+ }
622
+ {
623
+ print
624
+ }
625
+ END {
626
+ flush_missing()
627
+ }
628
+ ' "$WORK_ALLY_ASSISTANT_REGISTRY_FILE" > "$tmp"
629
+
630
+ mv "$tmp" "$WORK_ALLY_ASSISTANT_REGISTRY_FILE"
631
+ }
632
+
633
+ assistant_profile_rewrite_workspace_reference() {
634
+ local target_file="$1"
635
+ local old_workspace="$2"
636
+ local new_workspace="$3"
637
+
638
+ [ -f "$target_file" ] || return 0
639
+ [ -n "$old_workspace" ] || return 0
640
+ [ "$old_workspace" = "$new_workspace" ] && return 0
641
+
642
+ work_ally_require_cmd python3
643
+ OLD_WORKSPACE="$old_workspace" NEW_WORKSPACE="$new_workspace" \
644
+ python3 - "$target_file" <<'PY'
645
+ from pathlib import Path
646
+ import os
647
+ import sys
648
+ path = Path(sys.argv[1])
649
+ old = os.environ['OLD_WORKSPACE']
650
+ new = os.environ['NEW_WORKSPACE']
651
+ text = path.read_text()
652
+ path.write_text(text.replace(old, new))
653
+ PY
654
+ }
655
+
656
+ assistant_profile_rewrite_soul_name() {
657
+ local target_file="$1"
658
+ local old_name="$2"
659
+ local new_name="$3"
660
+
661
+ [ -f "$target_file" ] || return 0
662
+ [ -n "$old_name" ] || return 0
663
+ [ -n "$new_name" ] || return 0
664
+ [ "$old_name" = "$new_name" ] && return 0
665
+
666
+ work_ally_require_cmd python3
667
+ OLD_ASSISTANT_NAME="$old_name" NEW_ASSISTANT_NAME="$new_name" \
668
+ python3 - "$target_file" <<'PY'
669
+ from pathlib import Path
670
+ import os
671
+ import re
672
+ import sys
673
+ path = Path(sys.argv[1])
674
+ text = path.read_text()
675
+ pattern = rf'^(\s*-\s*名字:){re.escape(os.environ["OLD_ASSISTANT_NAME"])}$'
676
+ text, count = re.subn(pattern, rf'\1{os.environ["NEW_ASSISTANT_NAME"]}', text, count=1, flags=re.MULTILINE)
677
+ path.write_text(text)
678
+ PY
679
+ }
680
+
681
+ assistant_profile_rewrite_managed_env() {
682
+ local target_file="$1"
683
+ local new_name="$2"
684
+ local new_home="$3"
685
+ local new_codex_home="$4"
686
+ local new_state_dir="$5"
687
+ local tmp_file
688
+
689
+ [ -f "$target_file" ] || return 0
690
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/work-ally-managed-env.XXXXXX")
691
+
692
+ awk -v name="$new_name" -v home="$new_home" -v codex="$new_codex_home" -v state_dir="$new_state_dir" '
693
+ BEGIN {
694
+ wrote_name = 0
695
+ }
696
+ /^WORK_ALLY_ASSISTANT_NAME=/ {
697
+ print "WORK_ALLY_ASSISTANT_NAME=" name
698
+ wrote_name = 1
699
+ next
700
+ }
701
+ /^WORK_ALLY_ASSISTANT_HOME=/ {
702
+ print "WORK_ALLY_ASSISTANT_HOME=" home
703
+ next
704
+ }
705
+ /^WORK_ALLY_ASSISTANT_CODEX_HOME=/ {
706
+ print "WORK_ALLY_ASSISTANT_CODEX_HOME=" codex
707
+ next
708
+ }
709
+ /^WORK_ALLY_STATE_DIR=/ {
710
+ print "WORK_ALLY_STATE_DIR=" state_dir
711
+ next
712
+ }
713
+ {
714
+ print
715
+ }
716
+ END {
717
+ if (!wrote_name) {
718
+ print "WORK_ALLY_ASSISTANT_NAME=" name
719
+ }
720
+ }
721
+ ' "$target_file" > "$tmp_file"
722
+
723
+ mv "$tmp_file" "$target_file"
724
+ }
725
+
726
+ assistant_work_sessions_root() {
727
+ local assistant_home="$1"
728
+ printf '%s/.system/runtime/work-sessions\n' "$assistant_home"
729
+ }
730
+
731
+ assistant_work_session_blocking_summary() {
732
+ local root="$1"
733
+ local assistant_name="$2"
734
+
735
+ [ -d "$root" ] || return 0
736
+ work_ally_require_cmd python3
737
+ python3 - "$root" "$assistant_name" <<'PY'
738
+ from pathlib import Path
739
+ import json
740
+ import sys
741
+ root = Path(sys.argv[1])
742
+ assistant_name = sys.argv[2]
743
+ index_file = root / 'indexes' / 'assistants' / f'{assistant_name}.json'
744
+ if not index_file.exists():
745
+ raise SystemExit(0)
746
+ try:
747
+ index = json.loads(index_file.read_text())
748
+ except Exception:
749
+ raise SystemExit(0)
750
+ work_session_id = str(index.get('activeWorkSessionId') or '').strip()
751
+ if not work_session_id:
752
+ raise SystemExit(0)
753
+ meta_file = root / 'objects' / work_session_id / 'meta.json'
754
+ if not meta_file.exists():
755
+ raise SystemExit(0)
756
+ try:
757
+ meta = json.loads(meta_file.read_text())
758
+ except Exception:
759
+ raise SystemExit(0)
760
+ active_surface = str(meta.get('activeSurface') or '').strip()
761
+ archived_at = meta.get('archivedAt')
762
+ archived_text = '' if archived_at is None else str(archived_at).strip()
763
+ if not archived_text and active_surface in {'work_ally_channel', 'official_codex_cli'}:
764
+ sys.stdout.write(f'{active_surface}|{meta.get("workSessionId") or work_session_id}')
765
+ PY
766
+ }
767
+
768
+ assistant_work_session_migrate() {
769
+ local root="$1"
770
+ local old_name="$2"
771
+ local new_name="$3"
772
+ local old_codex_home="$4"
773
+ local new_codex_home="$5"
774
+
775
+ [ -d "$root" ] || return 0
776
+ work_ally_require_cmd python3
777
+ python3 - "$root" "$old_name" "$new_name" "$old_codex_home" "$new_codex_home" <<'PY'
778
+ from pathlib import Path
779
+ import json
780
+ import sys
781
+ root = Path(sys.argv[1])
782
+ old_name, new_name, old_codex_home, new_codex_home = sys.argv[2:6]
783
+ objects_dir = root / 'objects'
784
+ if objects_dir.exists():
785
+ for entry in objects_dir.iterdir():
786
+ if not entry.is_dir():
787
+ continue
788
+ meta_file = entry / 'meta.json'
789
+ if not meta_file.exists():
790
+ continue
791
+ try:
792
+ meta = json.loads(meta_file.read_text())
793
+ except Exception:
794
+ continue
795
+ changed = False
796
+ if meta.get('assistantName') == old_name:
797
+ meta['assistantName'] = new_name
798
+ changed = True
799
+ if meta.get('assistantCodexHome') == old_codex_home:
800
+ meta['assistantCodexHome'] = new_codex_home
801
+ changed = True
802
+ if changed:
803
+ meta_file.write_text(json.dumps(meta, indent=2) + '\n')
804
+ assistants_dir = root / 'indexes' / 'assistants'
805
+ old_index = assistants_dir / f'{old_name}.json'
806
+ new_index = assistants_dir / f'{new_name}.json'
807
+ if old_index.exists():
808
+ index = json.loads(old_index.read_text())
809
+ index['assistantName'] = new_name
810
+ new_index.parent.mkdir(parents=True, exist_ok=True)
811
+ new_index.write_text(json.dumps(index, indent=2) + '\n')
812
+ if new_index != old_index:
813
+ old_index.unlink()
814
+ PY
815
+ }
816
+
817
+ assistant_project_registry_rename_binding() {
818
+ local old_name="$1"
819
+ local new_name="$2"
820
+ local tmp_file
821
+
822
+ [ -f "$WORK_ALLY_PROJECT_REGISTRY_FILE" ] || return 0
823
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/work-ally-project-registry.XXXXXX")
824
+
825
+ awk -v old_name="$old_name" -v new_name="$new_name" '
826
+ {
827
+ if ($0 == " assistant: " old_name) {
828
+ print " assistant: " new_name
829
+ next
830
+ }
831
+ print
832
+ }
833
+ ' "$WORK_ALLY_PROJECT_REGISTRY_FILE" > "$tmp_file"
834
+
835
+ mv "$tmp_file" "$WORK_ALLY_PROJECT_REGISTRY_FILE"
836
+ }
837
+
838
+ assistant_registry_rename_entry() {
839
+ local old_name="$1"
840
+ local new_name="$2"
841
+ local new_home="$3"
842
+ local new_codex_home="$4"
843
+ local workspace_root="$5"
844
+ local description created_at
845
+
846
+ description=$(work_ally_assistant_description "$old_name")
847
+ created_at=$(work_ally_registry_field "$old_name" created_at || true)
848
+
849
+ work_ally_registry_remove_assistant "$old_name"
850
+ build_registry_entry "$new_name" "$new_home" "$new_codex_home" "$workspace_root" "$description" "$created_at" >> "$WORK_ALLY_ASSISTANT_REGISTRY_FILE"
851
+ }
852
+
853
+ assistant_lock_rewrite_name() {
854
+ local lock_file="$1"
855
+ local assistant_name="$2"
856
+ local tmp_file
857
+
858
+ [ -f "$lock_file" ] || return 0
859
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/work-ally-assistant-lock.XXXXXX")
860
+
861
+ awk -v assistant_name="$assistant_name" '
862
+ /^assistant=/ {
863
+ print "assistant=" assistant_name
864
+ next
865
+ }
866
+ {
867
+ print
868
+ }
869
+ ' "$lock_file" > "$tmp_file"
870
+
871
+ mv "$tmp_file" "$lock_file"
872
+ }
873
+
874
+ bind_assistant() {
875
+ local name="${1:-}"
876
+ local workspace_root=""
877
+ local assistant_home codex_home description existing_workspace
878
+
879
+ shift || true
880
+ [ -n "$name" ] || work_ally_die "Assistant name is required"
881
+ work_ally_require_registered_assistant "$name"
882
+
883
+ while [ "$#" -gt 0 ]; do
884
+ case "$1" in
885
+ --workspace)
886
+ [ "$#" -ge 2 ] || work_ally_die "--workspace requires a value"
887
+ workspace_root="$2"
888
+ shift 2
889
+ ;;
890
+ --workspace=*)
891
+ workspace_root="${1#--workspace=}"
892
+ shift
893
+ ;;
894
+ *)
895
+ work_ally_die "Unknown argument: $1"
896
+ ;;
897
+ esac
898
+ done
899
+
900
+ [ -n "$workspace_root" ] || work_ally_die "--workspace is required"
901
+ [ -d "$workspace_root" ] || work_ally_die "Workspace directory not found: $workspace_root"
902
+ workspace_root=$(cd "$workspace_root" && pwd)
903
+
904
+ assistant_home=$(work_ally_assistant_registered_home "$name")
905
+ codex_home=$(work_ally_assistant_registered_codex_home "$name")
906
+ description=$(work_ally_assistant_description "$name")
907
+ existing_workspace=$(work_ally_assistant_registered_workspace "$name" || true)
908
+ if [ -z "$existing_workspace" ]; then
909
+ existing_workspace=$(work_ally_registry_field "$name" workspace_root || true)
910
+ fi
911
+
912
+ if [ "$existing_workspace" = "$workspace_root" ]; then
913
+ work_ally_ok "Assistant $name 已经绑定到目标项目"
914
+ printf 'assistant_home: %s\n' "$assistant_home"
915
+ printf 'workspace: %s\n' "$workspace_root"
916
+ return 0
917
+ fi
918
+
919
+ if work_ally_assistant_is_busy "$name"; then
920
+ work_ally_die "Assistant $name is currently running. Stop it before rebinding."
921
+ fi
922
+
923
+ assistant_profile_remove_trusted_project "$codex_home/config.toml" "$existing_workspace"
924
+ assistant_profile_ensure_trusted_project "$codex_home/config.toml" "$workspace_root"
925
+ assistant_profile_rewrite_workspace_reference "$assistant_home/AGENTS.md" "$existing_workspace" "$workspace_root"
926
+ assistant_profile_rewrite_workspace_reference "$codex_home/AGENTS.md" "$existing_workspace" "$workspace_root"
927
+ assistant_registry_update_binding "$name" "$workspace_root"
928
+ work_ally_project_registry_set "$workspace_root" "$name"
929
+ work_ally_assistant_git_checkpoint "$assistant_home" "rebind assistant $name to $workspace_root" || true
930
+
931
+ work_ally_ok "Assistant binding updated for $name"
932
+ printf 'assistant_home: %s\n' "$assistant_home"
933
+ printf 'workspace: %s\n' "$workspace_root"
934
+ [ -n "$description" ] && printf 'description: %s\n' "$description"
935
+ }
936
+
937
+ list_assistants() {
938
+ if [ ! -f "$WORK_ALLY_ASSISTANT_REGISTRY_FILE" ]; then
939
+ echo 'No assistants registered yet.'
940
+ return 0
941
+ fi
942
+
943
+ awk '
944
+ /^ [^[:space:]]+:$/ {
945
+ name=$0
946
+ sub(/^ /, "", name)
947
+ sub(/:$/, "", name)
948
+ current=name
949
+ names[++count]=name
950
+ next
951
+ }
952
+ current && /^ description: / {
953
+ value=$0
954
+ sub(/^ description: /, "", value)
955
+ descriptions[current]=value
956
+ next
957
+ }
958
+ current && /^ codex_home: / {
959
+ value=$0
960
+ sub(/^ codex_home: /, "", value)
961
+ codex[current]=value
962
+ next
963
+ }
964
+ END {
965
+ if (!count) {
966
+ print "No assistants registered yet."
967
+ exit 0
968
+ }
969
+ for (i=1; i<=count; i++) {
970
+ name=names[i]
971
+ printf "%s\n", name
972
+ if (descriptions[name] != "") printf " description: %s\n", descriptions[name]
973
+ if (codex[name] != "") printf " codex_home: %s\n", codex[name]
974
+ }
975
+ }
976
+ ' "$WORK_ALLY_ASSISTANT_REGISTRY_FILE"
977
+ }
978
+
979
+ create_or_ensure_assistant() {
980
+ local mode="$1"
981
+ shift
982
+ local name="${1:-}"
983
+ shift || true
984
+ [ -n "$name" ] || work_ally_die "Assistant name is required"
985
+ work_ally_validate_assistant_name "$name"
986
+
987
+ local description="Assistant profile for $name"
988
+ local workspace_root=""
989
+ local existing_workspace=""
990
+ local git_remote=""
991
+ while [ "$#" -gt 0 ]; do
992
+ case "$1" in
993
+ --description)
994
+ [ "$#" -ge 2 ] || work_ally_die "--description requires a value"
995
+ description="$2"
996
+ shift 2
997
+ ;;
998
+ --description=*)
999
+ description="${1#--description=}"
1000
+ shift
1001
+ ;;
1002
+ --workspace)
1003
+ [ "$#" -ge 2 ] || work_ally_die "--workspace requires a value"
1004
+ workspace_root="$2"
1005
+ shift 2
1006
+ ;;
1007
+ --workspace=*)
1008
+ workspace_root="${1#--workspace=}"
1009
+ shift
1010
+ ;;
1011
+ --git-remote)
1012
+ [ "$#" -ge 2 ] || work_ally_die "--git-remote requires a value"
1013
+ git_remote="$2"
1014
+ shift 2
1015
+ ;;
1016
+ --git-remote=*)
1017
+ git_remote="${1#--git-remote=}"
1018
+ shift
1019
+ ;;
1020
+ *)
1021
+ work_ally_die "Unknown argument: $1"
1022
+ ;;
1023
+ esac
1024
+ done
1025
+
1026
+ [ -n "$workspace_root" ] || work_ally_die "--workspace is required"
1027
+ [ -d "$workspace_root" ] || work_ally_die "Workspace directory not found: $workspace_root"
1028
+ workspace_root=$(cd "$workspace_root" && pwd)
1029
+ export WORK_ALLY_WORKSPACE_ROOT="$workspace_root"
1030
+
1031
+ if work_ally_registry_has_assistant "$name"; then
1032
+ existing_workspace=$(work_ally_assistant_registered_workspace "$name" || true)
1033
+ if [ -n "$existing_workspace" ] && [ "$existing_workspace" != "$workspace_root" ]; then
1034
+ work_ally_die "Assistant $name is already bound to $existing_workspace. Create another assistant for $workspace_root."
1035
+ fi
1036
+ fi
1037
+
1038
+ if [ "$mode" = add ] && work_ally_registry_has_assistant "$name"; then
1039
+ work_ally_die "Assistant already registered: $name"
1040
+ fi
1041
+
1042
+ local homes assistant_home codex_home
1043
+ homes=$(bootstrap_assistant "$name" "$workspace_root" "$description" "$git_remote" "$existing_workspace")
1044
+ assistant_home=$(printf '%s\n' "$homes" | sed -n '1p')
1045
+ codex_home=$(printf '%s\n' "$homes" | sed -n '2p')
1046
+ ensure_registry_entry "$name" "$assistant_home" "$codex_home" "$workspace_root" "$description"
1047
+
1048
+ work_ally_ok "Assistant desk ready for $name"
1049
+ printf 'assistant_home: %s\n' "$assistant_home"
1050
+ printf 'codex_home: %s\n' "$codex_home"
1051
+ printf 'workspace: %s\n' "$workspace_root"
1052
+ }
1053
+
1054
+ show_assistant() {
1055
+ local name="${1:-}"
1056
+ local assistant_home
1057
+ [ -n "$name" ] || work_ally_die "Assistant name is required"
1058
+ work_ally_require_registered_assistant "$name"
1059
+ assistant_home=$(work_ally_assistant_registered_home "$name")
1060
+ printf 'name: %s\n' "$name"
1061
+ printf 'assistant_home: %s\n' "$assistant_home"
1062
+ printf 'codex_home: %s\n' "$(work_ally_assistant_registered_codex_home "$name")"
1063
+ printf 'workspace: %s\n' "$(work_ally_assistant_registered_workspace "$name")"
1064
+ printf 'description: %s\n' "$(work_ally_assistant_description "$name")"
1065
+ printf 'git_remote: %s\n' "$(work_ally_assistant_git_origin_url "$assistant_home" || true)"
1066
+ printf 'git_branch: %s\n' "$(work_ally_assistant_git_branch "$assistant_home")"
1067
+ }
1068
+
1069
+ confirm_assistant_removal() {
1070
+ local name="$1"
1071
+ local assistant_home="$2"
1072
+ local attempt reply
1073
+
1074
+ printf 'Danger: this will permanently delete assistant %s.\n' "$name" >&2
1075
+ printf 'Desk: %s\n' "$assistant_home" >&2
1076
+ printf 'It will also remove all project-to-assistant mappings for this assistant.\n' >&2
1077
+
1078
+ for attempt in 1 2 3; do
1079
+ printf 'Type YES to continue (%s/3): ' "$attempt" >&2
1080
+ if ! IFS= read -r reply; then
1081
+ work_ally_die "Assistant removal aborted"
1082
+ fi
1083
+ if [ "$reply" != "YES" ]; then
1084
+ work_ally_die "Assistant removal aborted"
1085
+ fi
1086
+ done
1087
+ }
1088
+
1089
+ remove_assistant() {
1090
+ local name="${1:-}"
1091
+ local assistant_home lock_file lock_workspace
1092
+ [ -n "$name" ] || work_ally_die "Assistant name is required"
1093
+ work_ally_require_registered_assistant "$name"
1094
+ assistant_home=$(work_ally_assistant_registered_home "$name")
1095
+ [ -n "$assistant_home" ] || work_ally_die "Assistant profile is invalid: missing assistant_home for $name"
1096
+
1097
+ if work_ally_assistant_is_busy "$name"; then
1098
+ lock_file=$(work_ally_assistant_lock_file "$name")
1099
+ lock_workspace=$(work_ally_assistant_lock_field "$lock_file" workspace_root || true)
1100
+ work_ally_die "Assistant $name is currently running${lock_workspace:+ in project $lock_workspace}. Stop it before removing."
1101
+ fi
1102
+
1103
+ confirm_assistant_removal "$name" "$assistant_home"
1104
+
1105
+ work_ally_project_registry_remove_assistant "$name"
1106
+ work_ally_registry_remove_assistant "$name"
1107
+
1108
+ lock_file=$(work_ally_assistant_lock_file "$name")
1109
+ rm -f "$lock_file"
1110
+ rm -rf "$assistant_home"
1111
+
1112
+ unset WORK_ALLY_ASSISTANT_HOME WORK_ALLY_STATE_DIR WORK_ALLY_RUNTIME_DIR \
1113
+ WORK_ALLY_LOG_DIR WORK_ALLY_SESSION_DIR WORK_ALLY_RUN_DIR \
1114
+ WORK_ALLY_CACHE_DIR WORK_ALLY_ENV_FILE WORK_ALLY_ASSISTANT_STATE_FILE
1115
+ printf '✓ Assistant removed: %s\n' "$name"
1116
+ }
1117
+
1118
+ rename_assistant_apply() {
1119
+ local old_name="$1"
1120
+ local new_name="$2"
1121
+ local old_home="$3"
1122
+ local new_home="$4"
1123
+ local old_codex_home="$5"
1124
+ local new_codex_home="$6"
1125
+ local workspace_root="$7"
1126
+ local old_lock="$8"
1127
+ local new_lock="$9"
1128
+ local new_state_dir="$new_home/.system"
1129
+ local config_file="$new_codex_home/config.toml"
1130
+
1131
+ mv "$old_home" "$new_home" || return 1
1132
+
1133
+ assistant_profile_rewrite_workspace_reference "$new_home/AGENTS.md" "$old_home" "$new_home" || return 1
1134
+ assistant_profile_rewrite_workspace_reference "$new_home/.system/codex-home/AGENTS.md" "$old_home" "$new_home" || return 1
1135
+ assistant_profile_rewrite_workspace_reference "$config_file" "$old_home" "$new_home" || return 1
1136
+ assistant_profile_remove_trusted_project "$config_file" "$old_home" || return 1
1137
+ assistant_profile_ensure_trusted_project "$config_file" "$new_home" || return 1
1138
+ assistant_profile_ensure_runtime_defaults "$config_file" "$new_home" || return 1
1139
+ assistant_profile_rewrite_soul_name "$new_home/SOUL.md" "$old_name" "$new_name" || return 1
1140
+ assistant_profile_rewrite_managed_env "$new_home/.system/config.env" "$new_name" "$new_home" "$new_codex_home" "$new_state_dir" || return 1
1141
+ assistant_profile_rewrite_managed_env "$new_home/.system/runtime/assistant.env" "$new_name" "$new_home" "$new_codex_home" "$new_state_dir" || return 1
1142
+ assistant_work_session_migrate "$(assistant_work_sessions_root "$new_home")" "$old_name" "$new_name" "$old_codex_home" "$new_codex_home" || return 1
1143
+ assistant_project_registry_rename_binding "$old_name" "$new_name" || return 1
1144
+ assistant_registry_rename_entry "$old_name" "$new_name" "$new_home" "$new_codex_home" "$workspace_root" || return 1
1145
+
1146
+ if [ -f "$old_lock" ]; then
1147
+ mv "$old_lock" "$new_lock" || return 1
1148
+ assistant_lock_rewrite_name "$new_lock" "$new_name" || return 1
1149
+ fi
1150
+
1151
+ return 0
1152
+ }
1153
+
1154
+ rename_assistant_restore_from_backup() {
1155
+ local backup_dir="$1"
1156
+ local old_home="$2"
1157
+ local new_home="$3"
1158
+ local old_lock="$4"
1159
+ local new_lock="$5"
1160
+
1161
+ rm -f "$new_lock"
1162
+
1163
+ if [ -d "$new_home" ]; then
1164
+ rm -rf "$new_home"
1165
+ fi
1166
+
1167
+ if [ -f "$backup_dir/assistant-registry.yaml" ]; then
1168
+ mkdir -p "$(dirname "$WORK_ALLY_ASSISTANT_REGISTRY_FILE")"
1169
+ cp "$backup_dir/assistant-registry.yaml" "$WORK_ALLY_ASSISTANT_REGISTRY_FILE"
1170
+ else
1171
+ rm -f "$WORK_ALLY_ASSISTANT_REGISTRY_FILE"
1172
+ fi
1173
+
1174
+ if [ -f "$backup_dir/project-registry.yaml" ]; then
1175
+ mkdir -p "$(dirname "$WORK_ALLY_PROJECT_REGISTRY_FILE")"
1176
+ cp "$backup_dir/project-registry.yaml" "$WORK_ALLY_PROJECT_REGISTRY_FILE"
1177
+ else
1178
+ rm -f "$WORK_ALLY_PROJECT_REGISTRY_FILE"
1179
+ fi
1180
+
1181
+ if [ -f "$backup_dir/assistant.lock" ]; then
1182
+ mkdir -p "$(dirname "$old_lock")"
1183
+ cp "$backup_dir/assistant.lock" "$old_lock"
1184
+ else
1185
+ rm -f "$old_lock"
1186
+ fi
1187
+
1188
+ if [ ! -d "$old_home" ] && [ -d "$backup_dir/assistant-home" ]; then
1189
+ mv "$backup_dir/assistant-home" "$old_home" || return 1
1190
+ fi
1191
+
1192
+ return 0
1193
+ }
1194
+
1195
+ rename_assistant() {
1196
+ local old_name="${1:-}"
1197
+ local new_name="${2:-}"
1198
+ local old_home old_codex_home expected_old_home expected_old_codex_home
1199
+ local new_home new_codex_home workspace_root old_lock new_lock new_work_session_index
1200
+ local lock_workspace blocking_summary active_surface work_session_id backup_dir
1201
+
1202
+ [ -n "$old_name" ] || work_ally_die "Old assistant name is required"
1203
+ [ -n "$new_name" ] || work_ally_die "New assistant name is required"
1204
+ shift 2 || true
1205
+ [ "$#" -eq 0 ] || work_ally_die "Unknown argument: $1"
1206
+
1207
+ work_ally_require_registered_assistant "$old_name"
1208
+ work_ally_validate_assistant_name "$new_name"
1209
+ [ "$old_name" != "$new_name" ] || work_ally_die "Old and new assistant names must differ"
1210
+
1211
+ if work_ally_registry_has_assistant "$new_name"; then
1212
+ work_ally_die "Assistant already registered: $new_name"
1213
+ fi
1214
+
1215
+ old_home=$(work_ally_assistant_registered_home "$old_name")
1216
+ old_codex_home=$(work_ally_assistant_registered_codex_home "$old_name")
1217
+ workspace_root=$(work_ally_assistant_registered_workspace "$old_name" || true)
1218
+ if [ -z "$workspace_root" ]; then
1219
+ workspace_root=$(work_ally_registry_field "$old_name" workspace_root || true)
1220
+ fi
1221
+
1222
+ expected_old_home=$(work_ally_assistant_home "$old_name")
1223
+ expected_old_codex_home=$(work_ally_assistant_codex_home "$old_name")
1224
+ [ -n "$old_home" ] || work_ally_die "Assistant profile is invalid: missing assistant_home for $old_name"
1225
+ [ -n "$old_codex_home" ] || work_ally_die "Assistant profile is invalid: missing codex_home for $old_name"
1226
+ [ -n "$workspace_root" ] || work_ally_die "Assistant $old_name is not bound to a project yet."
1227
+ [ "$old_home" = "$expected_old_home" ] || work_ally_die "Assistant $old_name cannot be renamed safely because assistant_home is not canonical: $old_home"
1228
+ [ "$old_codex_home" = "$expected_old_codex_home" ] || work_ally_die "Assistant $old_name cannot be renamed safely because codex_home is not canonical: $old_codex_home"
1229
+ [ -d "$old_home" ] || work_ally_die "Assistant profile is incomplete: missing assistant_home at $old_home"
1230
+ [ -d "$old_codex_home" ] || work_ally_die "Assistant profile is incomplete: missing codex_home at $old_codex_home"
1231
+
1232
+ new_home=$(work_ally_assistant_home "$new_name")
1233
+ new_codex_home=$(work_ally_assistant_codex_home "$new_name")
1234
+ [ ! -e "$new_home" ] || work_ally_die "Assistant home already exists for $new_name: $new_home"
1235
+
1236
+ new_work_session_index="$(assistant_work_sessions_root "$old_home")/indexes/assistants/$new_name.json"
1237
+ [ ! -e "$new_work_session_index" ] || work_ally_die "Assistant work-session index already exists for $new_name: $new_work_session_index"
1238
+
1239
+ old_lock=$(work_ally_assistant_lock_file "$old_name")
1240
+ new_lock=$(work_ally_assistant_lock_file "$new_name")
1241
+ [ ! -e "$new_lock" ] || work_ally_die "Assistant lock already exists for $new_name: $new_lock"
1242
+
1243
+ if work_ally_assistant_is_busy "$old_name"; then
1244
+ lock_workspace=$(work_ally_assistant_lock_field "$old_lock" workspace_root || true)
1245
+ work_ally_die "Assistant $old_name is currently running${lock_workspace:+ in project $lock_workspace}. Stop it before renaming."
1246
+ fi
1247
+
1248
+ blocking_summary=$(assistant_work_session_blocking_summary "$(assistant_work_sessions_root "$old_home")" "$old_name" || true)
1249
+ if [ -n "$blocking_summary" ]; then
1250
+ active_surface=${blocking_summary%%|*}
1251
+ work_session_id=${blocking_summary#*|}
1252
+ work_ally_die "Assistant $old_name still has an active work session on $active_surface ($work_session_id). Close or idle it before renaming."
1253
+ fi
1254
+
1255
+ backup_dir=$(mktemp -d "${TMPDIR:-/tmp}/work-ally-assistant-rename.XXXXXX")
1256
+ cp -R "$old_home" "$backup_dir/assistant-home" || work_ally_die "Failed to stage assistant backup for $old_name"
1257
+ [ ! -f "$WORK_ALLY_ASSISTANT_REGISTRY_FILE" ] || cp "$WORK_ALLY_ASSISTANT_REGISTRY_FILE" "$backup_dir/assistant-registry.yaml"
1258
+ [ ! -f "$WORK_ALLY_PROJECT_REGISTRY_FILE" ] || cp "$WORK_ALLY_PROJECT_REGISTRY_FILE" "$backup_dir/project-registry.yaml"
1259
+ [ ! -f "$old_lock" ] || cp "$old_lock" "$backup_dir/assistant.lock"
1260
+
1261
+ if ! rename_assistant_apply "$old_name" "$new_name" "$old_home" "$new_home" "$old_codex_home" "$new_codex_home" "$workspace_root" "$old_lock" "$new_lock"; then
1262
+ if ! rename_assistant_restore_from_backup "$backup_dir" "$old_home" "$new_home" "$old_lock" "$new_lock"; then
1263
+ rm -rf "$backup_dir"
1264
+ work_ally_die "Assistant rename failed and rollback failed. Inspect $old_home, $new_home and the registries manually."
1265
+ fi
1266
+ rm -rf "$backup_dir"
1267
+ work_ally_die "Assistant rename from $old_name to $new_name failed. Original state was restored."
1268
+ fi
1269
+
1270
+ rm -rf "$backup_dir"
1271
+
1272
+ export WORK_ALLY_WORKSPACE_ROOT="$workspace_root"
1273
+ work_ally_hydrate_assistant_context "$new_name"
1274
+ work_ally_assistant_git_checkpoint "$new_home" "rename assistant $old_name to $new_name" || true
1275
+
1276
+ work_ally_ok "Assistant renamed from $old_name to $new_name"
1277
+ printf 'old_name: %s\n' "$old_name"
1278
+ printf 'new_name: %s\n' "$new_name"
1279
+ printf 'assistant_home: %s\n' "$new_home"
1280
+ printf 'codex_home: %s\n' "$new_codex_home"
1281
+ printf 'workspace: %s\n' "$workspace_root"
1282
+ }
1283
+
1284
+ case "$SUBCMD" in
1285
+ add)
1286
+ create_or_ensure_assistant add "$@"
1287
+ ;;
1288
+ ensure)
1289
+ create_or_ensure_assistant ensure "$@"
1290
+ ;;
1291
+ bind)
1292
+ bind_assistant "$@"
1293
+ ;;
1294
+ remove)
1295
+ remove_assistant "$@"
1296
+ ;;
1297
+ rename)
1298
+ rename_assistant "$@"
1299
+ ;;
1300
+ list)
1301
+ list_assistants
1302
+ ;;
1303
+ show)
1304
+ show_assistant "$@"
1305
+ ;;
1306
+ help|-h|--help)
1307
+ usage
1308
+ ;;
1309
+ *)
1310
+ work_ally_die "Unknown assistant subcommand: $SUBCMD"
1311
+ ;;
1312
+ esac