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,364 @@
1
+ import path from 'node:path';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { loadConfig } from './config.ts';
4
+ import { FakeRuntimeClient } from './fake-runtime-client.ts';
5
+ import { HandoffService, type HandoffExportPayload } from './handoff-service.ts';
6
+ import { CodexRuntimeClient } from './runtime-client.ts';
7
+ import type { WorkSessionMeta } from './types.ts';
8
+ import { WorkSessionStore } from './work-session-store.ts';
9
+
10
+ type CommandFailureReason = HandoffExportPayload['reason'] | 'candidate_missing' | 'candidate_ambiguous' | 'thread_handle_not_found';
11
+
12
+ interface ManagedThreadCommandPayload extends Omit<HandoffExportPayload, 'reason'> {
13
+ status: HandoffExportPayload['status'] | 'ambiguous';
14
+ reason?: CommandFailureReason;
15
+ threadHandle: string | null;
16
+ candidateCount: number;
17
+ selector: {
18
+ mode: 'new' | 'default' | 'thread_handle';
19
+ value: string | null;
20
+ };
21
+ availableHandles?: string[];
22
+ }
23
+
24
+ interface ThreadsPayload {
25
+ assistant: string;
26
+ workspaceRoot: string;
27
+ assistantCodexHome: string | null;
28
+ defaultWorkSessionId: string | null;
29
+ threads: Array<{
30
+ workSessionId: string;
31
+ threadHandle: string;
32
+ threadName: string | null;
33
+ activeSurface: WorkSessionMeta['activeSurface'];
34
+ origin: WorkSessionMeta['origin'];
35
+ updatedAt: string;
36
+ archivedAt: string | null;
37
+ isDefault: boolean;
38
+ }>;
39
+ }
40
+
41
+ function printJson(value: unknown): void {
42
+ console.log(JSON.stringify(value, null, 2));
43
+ }
44
+
45
+ function commandArgs(): string[] {
46
+ return process.argv.slice(4);
47
+ }
48
+
49
+ function hasFlag(flag: string): boolean {
50
+ return commandArgs().includes(flag);
51
+ }
52
+
53
+ function threadHandleFromArgs(args: string[]): string | null {
54
+ for (let index = 0; index < args.length; index += 1) {
55
+ const value = args[index];
56
+ if (value === '--thread') {
57
+ return String(args[index + 1] || '').trim().toLowerCase() || null;
58
+ }
59
+ if (value.startsWith('--thread=')) {
60
+ return value.slice('--thread='.length).trim().toLowerCase() || null;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ function assistantNameFromConfig(config: ReturnType<typeof loadConfig>): string {
67
+ return String(config.assistant?.name || '').trim() || 'native';
68
+ }
69
+
70
+ function managedThreadCandidates(store: WorkSessionStore, assistantName: string): WorkSessionMeta[] {
71
+ return store
72
+ .listWorkSessionsForAssistant(assistantName)
73
+ .filter((session) => session.activeSurface !== 'closed');
74
+ }
75
+
76
+ function buildFailurePayload(
77
+ config: ReturnType<typeof loadConfig>,
78
+ reason: CommandFailureReason,
79
+ selector: ManagedThreadCommandPayload['selector'],
80
+ candidateCount: number,
81
+ availableHandles: string[] = [],
82
+ ): ManagedThreadCommandPayload {
83
+ return {
84
+ status: reason === 'candidate_ambiguous' ? 'ambiguous' : 'not_found',
85
+ reason,
86
+ assistantName: assistantNameFromConfig(config),
87
+ workspaceRoot: config.workspaceRoot,
88
+ assistantCodexHome: config.assistant?.codexHome || null,
89
+ workSessionId: null,
90
+ threadHandle: null,
91
+ runtimeThreadId: null,
92
+ cliResumeRef: null,
93
+ cliResumeRefType: 'null',
94
+ threadName: null,
95
+ activeSurface: null,
96
+ ownershipSource: null,
97
+ resumeCapability: 'not_ready',
98
+ command: null,
99
+ exportedAt: new Date().toISOString(),
100
+ candidateCount,
101
+ selector,
102
+ availableHandles,
103
+ };
104
+ }
105
+
106
+ function buildManagedPayload(
107
+ payload: HandoffExportPayload,
108
+ target: WorkSessionMeta,
109
+ selector: ManagedThreadCommandPayload['selector'],
110
+ candidateCount: number,
111
+ ): ManagedThreadCommandPayload {
112
+ return {
113
+ ...payload,
114
+ threadHandle: target.threadHandle,
115
+ candidateCount,
116
+ selector,
117
+ };
118
+ }
119
+
120
+ function printThreads(payload: ThreadsPayload): void {
121
+ if (!payload.threads.length) {
122
+ console.log('当前还没有可继续的受管线程。');
123
+ return;
124
+ }
125
+
126
+ console.log('当前受管线程:');
127
+ for (const thread of payload.threads) {
128
+ const badges = [];
129
+ if (thread.isDefault) {
130
+ badges.push('default');
131
+ }
132
+ if (thread.origin === 'codex_cli_attach') {
133
+ badges.push('attach');
134
+ }
135
+ if (thread.archivedAt) {
136
+ badges.push('archived');
137
+ }
138
+ const badgeText = badges.length ? ` [${badges.join(', ')}]` : '';
139
+ const preview = String(thread.threadName || '').trim() || '无标题';
140
+ console.log(`- ${thread.threadHandle}${badgeText} ${preview}`);
141
+ console.log(` surface=${thread.activeSurface} updated=${thread.updatedAt}`);
142
+ }
143
+ }
144
+
145
+ function openReadyPayload(
146
+ payload: Pick<HandoffExportPayload, 'status' | 'command' | 'cliResumeRef' | 'workSessionId' | 'assistantCodexHome' | 'workspaceRoot'>,
147
+ service: HandoffService,
148
+ ): void {
149
+ if (payload.status !== 'ready' || !payload.command || !payload.cliResumeRef || !payload.workSessionId) {
150
+ printJson(payload);
151
+ process.exit(1);
152
+ }
153
+
154
+ service.markWorkSessionOfficialCli(payload.workSessionId);
155
+ const args = ['resume', '--cd', payload.workspaceRoot, payload.cliResumeRef];
156
+ const env = payload.assistantCodexHome
157
+ ? { ...process.env, CODEX_HOME: payload.assistantCodexHome }
158
+ : { ...process.env };
159
+ const result = spawnSync('codex', args, { stdio: 'inherit', env });
160
+ if (typeof result.status === 'number') {
161
+ process.exit(result.status);
162
+ }
163
+ if (result.error) {
164
+ throw result.error;
165
+ }
166
+ }
167
+
168
+ function createRuntimeClient(config: ReturnType<typeof loadConfig>) {
169
+ if (config.runtime.mode === 'fake') {
170
+ return new FakeRuntimeClient();
171
+ }
172
+ return new CodexRuntimeClient({ codexHome: config.assistant?.codexHome || null });
173
+ }
174
+
175
+ async function runNewCommand(
176
+ config: ReturnType<typeof loadConfig>,
177
+ store: WorkSessionStore,
178
+ service: HandoffService,
179
+ ): Promise<void> {
180
+ const runtime = createRuntimeClient(config);
181
+ try {
182
+ const thread = await runtime.startThread(config.workspaceRoot);
183
+ const created = store.createWorkSession({
184
+ assistantName: assistantNameFromConfig(config),
185
+ assistantCodexHome: config.assistant?.codexHome || null,
186
+ workspaceRoot: config.workspaceRoot,
187
+ runtimeThreadId: thread.id,
188
+ cliResumeRef: thread.id,
189
+ cliResumeRefType: 'session_id',
190
+ origin: 'work_ally',
191
+ activeSurface: 'official_codex_cli',
192
+ ownershipSource: 'explicit_codex_launch',
193
+ });
194
+ const payload = buildManagedPayload(
195
+ await service.exportCurrentWorkSession(created),
196
+ created,
197
+ { mode: 'new', value: created.threadHandle },
198
+ 1,
199
+ );
200
+
201
+ if (hasFlag('--json')) {
202
+ printJson(payload);
203
+ if (payload.status !== 'ready') {
204
+ process.exit(1);
205
+ }
206
+ return;
207
+ }
208
+
209
+ if (hasFlag('--print')) {
210
+ if (payload.status !== 'ready' || !payload.command) {
211
+ printJson(payload);
212
+ process.exit(1);
213
+ }
214
+ console.log(payload.command);
215
+ return;
216
+ }
217
+
218
+ openReadyPayload(payload, service);
219
+ } finally {
220
+ await runtime.disconnect();
221
+ }
222
+ }
223
+
224
+ async function runContinueCommand(
225
+ config: ReturnType<typeof loadConfig>,
226
+ store: WorkSessionStore,
227
+ service: HandoffService,
228
+ ): Promise<void> {
229
+ const assistantName = assistantNameFromConfig(config);
230
+ const candidates = managedThreadCandidates(store, assistantName);
231
+ const requestedHandle = threadHandleFromArgs(commandArgs());
232
+ const selector = requestedHandle
233
+ ? { mode: 'thread_handle' as const, value: requestedHandle }
234
+ : { mode: 'default' as const, value: null };
235
+
236
+ let target: WorkSessionMeta | null = null;
237
+ if (requestedHandle) {
238
+ const matched = store.findWorkSessionByThreadHandle(assistantName, requestedHandle);
239
+ if (matched && !matched.archivedAt && matched.activeSurface !== 'closed') {
240
+ target = matched;
241
+ }
242
+ } else if (candidates.length === 1) {
243
+ target = candidates[0];
244
+ }
245
+
246
+ const payload = target
247
+ ? buildManagedPayload(await service.exportCurrentWorkSession(target), target, selector, candidates.length)
248
+ : buildFailurePayload(
249
+ config,
250
+ requestedHandle
251
+ ? 'thread_handle_not_found'
252
+ : candidates.length === 0
253
+ ? 'candidate_missing'
254
+ : 'candidate_ambiguous',
255
+ selector,
256
+ candidates.length,
257
+ candidates.map((candidate) => candidate.threadHandle),
258
+ );
259
+
260
+ if (hasFlag('--json')) {
261
+ printJson(payload);
262
+ if (payload.status !== 'ready') {
263
+ process.exit(1);
264
+ }
265
+ return;
266
+ }
267
+
268
+ if (hasFlag('--print')) {
269
+ if (payload.status !== 'ready' || !payload.command) {
270
+ printJson(payload);
271
+ process.exit(1);
272
+ }
273
+ console.log(payload.command);
274
+ return;
275
+ }
276
+
277
+ openReadyPayload(payload, service);
278
+ }
279
+
280
+ function runThreadsCommand(config: ReturnType<typeof loadConfig>, store: WorkSessionStore): void {
281
+ const assistantName = assistantNameFromConfig(config);
282
+ const activeWorkSessionId = store.getAssistantIndex(assistantName)?.activeWorkSessionId || null;
283
+ const payload: ThreadsPayload = {
284
+ assistant: assistantName,
285
+ workspaceRoot: config.workspaceRoot,
286
+ assistantCodexHome: config.assistant?.codexHome || null,
287
+ defaultWorkSessionId: activeWorkSessionId,
288
+ threads: store.listWorkSessionsForAssistant(assistantName).map((thread) => ({
289
+ workSessionId: thread.workSessionId,
290
+ threadHandle: thread.threadHandle,
291
+ threadName: thread.threadName,
292
+ activeSurface: thread.activeSurface,
293
+ origin: thread.origin,
294
+ updatedAt: thread.updatedAt,
295
+ archivedAt: thread.archivedAt,
296
+ isDefault: thread.workSessionId === activeWorkSessionId,
297
+ })),
298
+ };
299
+
300
+ if (hasFlag('--json')) {
301
+ printJson(payload);
302
+ return;
303
+ }
304
+
305
+ printThreads(payload);
306
+ }
307
+
308
+ export async function runHandoffCommand(): Promise<void> {
309
+ const config = loadConfig();
310
+ const handoffKind = process.argv[3] || '';
311
+ const workSessionStore = new WorkSessionStore(config.paths.workSessionsDir || path.join(config.paths.runtimeDir, 'work-sessions'));
312
+ const service = new HandoffService(config, workSessionStore);
313
+
314
+ if (handoffKind === 'codex') {
315
+ const payload = await service.exportActiveWorkSessionForAssistant();
316
+ if (hasFlag('--print')) {
317
+ if (payload.status !== 'ready' || !payload.command) {
318
+ printJson(payload);
319
+ process.exit(1);
320
+ }
321
+ console.log(payload.command);
322
+ return;
323
+ }
324
+
325
+ if (hasFlag('--open')) {
326
+ openReadyPayload(payload, service);
327
+ return;
328
+ }
329
+
330
+ printJson(payload);
331
+ if (payload.status !== 'ready') {
332
+ process.exit(1);
333
+ }
334
+ return;
335
+ }
336
+
337
+ if (handoffKind === 'attach') {
338
+ const payload = await service.attachFromCliArgs(commandArgs());
339
+ printJson(payload);
340
+ if (payload.status !== 'attached') {
341
+ process.exit(1);
342
+ }
343
+ return;
344
+ }
345
+
346
+ if (handoffKind === 'threads') {
347
+ runThreadsCommand(config, workSessionStore);
348
+ return;
349
+ }
350
+
351
+ if (handoffKind === 'continue') {
352
+ await runContinueCommand(config, workSessionStore, service);
353
+ return;
354
+ }
355
+
356
+ if (handoffKind === 'new') {
357
+ await runNewCommand(config, workSessionStore, service);
358
+ return;
359
+ }
360
+
361
+ throw new Error(
362
+ 'unknown handoff subcommand: ' + (handoffKind || '(missing)'),
363
+ );
364
+ }
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import { loadConfig } from './config.ts';
3
+ import { initializeProcessLogger, logError, logInfo, logWarn } from './logger.ts';
4
+ import { createRuntimeApp, runRoutineCommand } from './server.ts';
5
+ import { runHandoffCommand } from './server-handoff-command.ts';
6
+ import { runThreadSyncCommand } from './server-thread-sync-command.ts';
7
+ import { nowIso, writeJsonFile } from './utils.ts';
8
+
9
+ export async function main(): Promise<void> {
10
+ if (process.argv[2] === 'routine') {
11
+ await runRoutineCommand();
12
+ return;
13
+ }
14
+
15
+ if (process.argv[2] === 'handoff') {
16
+ await runHandoffCommand();
17
+ return;
18
+ }
19
+
20
+ if (process.argv[2] === 'thread-sync') {
21
+ await runThreadSyncCommand();
22
+ return;
23
+ }
24
+
25
+ const config = loadConfig();
26
+ initializeProcessLogger({
27
+ logsDir: config.paths.logsDir,
28
+ component: 'bridge',
29
+ timezone: config.timezone,
30
+ retentionDays: config.logging.retentionDays,
31
+ mirrorToStdout: process.env.WORK_ALLY_FOREGROUND === '1',
32
+ });
33
+ logInfo('bridge', 'process_start', {
34
+ pid: process.pid,
35
+ workspaceRoot: config.workspaceRoot,
36
+ channel: config.channel.impl,
37
+ runtime: config.runtime.mode,
38
+ });
39
+ process.on('unhandledRejection', (reason) => {
40
+ logError('bridge', 'unhandled_rejection', {
41
+ reason: reason instanceof Error ? (reason.stack || reason.message) : String(reason),
42
+ });
43
+ });
44
+ process.on('uncaughtException', (error) => {
45
+ logError('bridge', 'uncaught_exception', {
46
+ error: error.stack || error.message,
47
+ });
48
+ });
49
+ process.on('beforeExit', (code) => {
50
+ logWarn('bridge', 'before_exit', { code });
51
+ });
52
+ process.on('exit', (code) => {
53
+ writeJsonFile(path.join(config.paths.runtimeDir, 'bridge.health.json'), {
54
+ status: 'stopped',
55
+ at: nowIso(),
56
+ pid: process.pid,
57
+ workspaceRoot: config.workspaceRoot,
58
+ channel: config.channel.impl,
59
+ runtime: config.runtime.mode,
60
+ exitCode: code,
61
+ });
62
+ logInfo('bridge', 'process_exit', { code });
63
+ });
64
+
65
+ const app = createRuntimeApp(config);
66
+ await app.start();
67
+
68
+ let shuttingDown = false;
69
+ const shutdown = async () => {
70
+ if (shuttingDown) {
71
+ return;
72
+ }
73
+ shuttingDown = true;
74
+ logInfo('bridge', 'shutdown_signal');
75
+ await app.stop();
76
+ process.exit(0);
77
+ };
78
+ process.on('SIGINT', shutdown);
79
+ process.on('SIGTERM', shutdown);
80
+ }
@@ -0,0 +1,60 @@
1
+ import path from 'node:path';
2
+ import { loadConfig, validateConfig } from './config.ts';
3
+ import { initializeProcessLogger, logInfo } from './logger.ts';
4
+ import { createRuntimeApp } from './server.ts';
5
+ import { Scheduler } from './scheduler.ts';
6
+
7
+ export async function runRoutineCommand(): Promise<void> {
8
+ const config = loadConfig();
9
+ validateConfig(config);
10
+ const subcmd = process.argv[3] || 'list';
11
+ initializeProcessLogger({
12
+ logsDir: config.paths.logsDir,
13
+ component: 'routine',
14
+ timezone: config.timezone,
15
+ retentionDays: config.logging.retentionDays,
16
+ captureConsole: false,
17
+ });
18
+ logInfo('routine', 'command_start', {
19
+ pid: process.pid,
20
+ subcmd,
21
+ workspaceRoot: config.workspaceRoot,
22
+ });
23
+ const scheduler = new Scheduler(config.paths.routinesDir, path.join(config.paths.runtimeDir, 'routines-state.json'), config.timezone, config.paths.projectRoutinesDir);
24
+
25
+ if (subcmd === 'list') {
26
+ for (const routine of scheduler.listRoutines()) {
27
+ console.log(`${routine.id}\t${routine.schedule || '(manual)'}\t${routine.enabled === false ? 'disabled' : 'enabled'}`);
28
+ }
29
+ return;
30
+ }
31
+
32
+ const id = process.argv[4];
33
+ if (!id) {
34
+ throw new Error('routine id is required');
35
+ }
36
+
37
+ if (subcmd === 'enable') {
38
+ scheduler.enableRoutine(id);
39
+ console.log(`enabled ${id}`);
40
+ return;
41
+ }
42
+
43
+ if (subcmd === 'disable') {
44
+ scheduler.disableRoutine(id);
45
+ console.log(`disabled ${id}`);
46
+ return;
47
+ }
48
+
49
+ if (subcmd === 'run') {
50
+ const app = createRuntimeApp(config);
51
+ try {
52
+ await app.runRoutine(id);
53
+ } finally {
54
+ await app.dispose();
55
+ }
56
+ return;
57
+ }
58
+
59
+ throw new Error(`unknown routine subcommand: ${subcmd}`);
60
+ }