@undefineds.co/linx 0.3.5 → 0.3.8

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/README.md +58 -23
  2. package/dist/generated/version.js +1 -1
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +336 -162
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/account-session.js +4 -8
  7. package/dist/lib/account-session.js.map +1 -1
  8. package/dist/lib/ai-command.js +228 -178
  9. package/dist/lib/ai-command.js.map +1 -1
  10. package/dist/lib/auto-mode/archive.js +38 -7
  11. package/dist/lib/auto-mode/archive.js.map +1 -1
  12. package/dist/lib/auto-mode/auth.js.map +1 -1
  13. package/dist/lib/auto-mode/display.js +71 -45
  14. package/dist/lib/auto-mode/display.js.map +1 -1
  15. package/dist/lib/auto-mode/format.js +9 -7
  16. package/dist/lib/auto-mode/format.js.map +1 -1
  17. package/dist/lib/auto-mode/hooks/claude.js +12 -2
  18. package/dist/lib/auto-mode/hooks/claude.js.map +1 -1
  19. package/dist/lib/auto-mode/hooks/codex.js +17 -7
  20. package/dist/lib/auto-mode/hooks/codex.js.map +1 -1
  21. package/dist/lib/auto-mode/hooks/index.js +28 -8
  22. package/dist/lib/auto-mode/hooks/index.js.map +1 -1
  23. package/dist/lib/auto-mode/pod-ai.js +20 -37
  24. package/dist/lib/auto-mode/pod-ai.js.map +1 -1
  25. package/dist/lib/auto-mode/pod-approval.js +124 -195
  26. package/dist/lib/auto-mode/pod-approval.js.map +1 -1
  27. package/dist/lib/auto-mode/pod-persistence.js +169 -90
  28. package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
  29. package/dist/lib/auto-mode/runner.js +683 -81
  30. package/dist/lib/auto-mode/runner.js.map +1 -1
  31. package/dist/lib/auto-mode/secretary.js +186 -41
  32. package/dist/lib/auto-mode/secretary.js.map +1 -1
  33. package/dist/lib/auto-mode-command.js +32 -32
  34. package/dist/lib/auto-mode-command.js.map +1 -1
  35. package/dist/lib/chat-api.js +242 -50
  36. package/dist/lib/chat-api.js.map +1 -1
  37. package/dist/lib/codex-plugin/bridge.js +164 -17
  38. package/dist/lib/codex-plugin/bridge.js.map +1 -1
  39. package/dist/lib/codex-plugin/codex-native-proxy.js +370 -34
  40. package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -1
  41. package/dist/lib/credentials-store.js +33 -42
  42. package/dist/lib/credentials-store.js.map +1 -1
  43. package/dist/lib/linx-cloud-errors.js +61 -0
  44. package/dist/lib/linx-cloud-errors.js.map +1 -0
  45. package/dist/lib/linx-tui-contract.js +8 -5
  46. package/dist/lib/linx-tui-contract.js.map +1 -1
  47. package/dist/lib/login-command.js +9 -2
  48. package/dist/lib/login-command.js.map +1 -1
  49. package/dist/lib/models.js +3 -20
  50. package/dist/lib/models.js.map +1 -1
  51. package/dist/lib/oidc-auth.js +143 -17
  52. package/dist/lib/oidc-auth.js.map +1 -1
  53. package/dist/lib/oidc-session-storage.js +2 -6
  54. package/dist/lib/oidc-session-storage.js.map +1 -1
  55. package/dist/lib/pi-adapter/auto-input-controller.js +988 -0
  56. package/dist/lib/pi-adapter/auto-input-controller.js.map +1 -0
  57. package/dist/lib/pi-adapter/backend-command.js +2 -0
  58. package/dist/lib/pi-adapter/backend-command.js.map +1 -0
  59. package/dist/lib/pi-adapter/backend-credentials.js +80 -0
  60. package/dist/lib/pi-adapter/backend-credentials.js.map +1 -0
  61. package/dist/lib/pi-adapter/branding.js +246 -108
  62. package/dist/lib/pi-adapter/branding.js.map +1 -1
  63. package/dist/lib/pi-adapter/control-state.js +72 -0
  64. package/dist/lib/pi-adapter/control-state.js.map +1 -0
  65. package/dist/lib/pi-adapter/interactive.js +2634 -30
  66. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  67. package/dist/lib/pi-adapter/pod-approval.js +382 -210
  68. package/dist/lib/pi-adapter/pod-approval.js.map +1 -1
  69. package/dist/lib/pi-adapter/pod-mirror-mapping.js +71 -17
  70. package/dist/lib/pi-adapter/pod-mirror-mapping.js.map +1 -1
  71. package/dist/lib/pi-adapter/pod-mirror.js +531 -64
  72. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  73. package/dist/lib/pi-adapter/pod-native.js +81 -85
  74. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  75. package/dist/lib/pi-adapter/pod-status-output.js +54 -0
  76. package/dist/lib/pi-adapter/pod-status-output.js.map +1 -0
  77. package/dist/lib/pi-adapter/runtime.js +458 -228
  78. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  79. package/dist/lib/pi-adapter/session-control.js +509 -0
  80. package/dist/lib/pi-adapter/session-control.js.map +1 -0
  81. package/dist/lib/pi-adapter/session.js +35 -22
  82. package/dist/lib/pi-adapter/session.js.map +1 -1
  83. package/dist/lib/pi-adapter/stream.js +89 -32
  84. package/dist/lib/pi-adapter/stream.js.map +1 -1
  85. package/dist/lib/pi-adapter/sync-recovery.js +89 -0
  86. package/dist/lib/pi-adapter/sync-recovery.js.map +1 -0
  87. package/dist/lib/pi-adapter/web-fetch.js +13 -14
  88. package/dist/lib/pi-adapter/web-fetch.js.map +1 -1
  89. package/dist/lib/pod-chat-store.js +254 -78
  90. package/dist/lib/pod-chat-store.js.map +1 -1
  91. package/dist/lib/pod-data-session.js +156 -35
  92. package/dist/lib/pod-data-session.js.map +1 -1
  93. package/dist/lib/solid-auth-store.js +27 -0
  94. package/dist/lib/solid-auth-store.js.map +1 -0
  95. package/dist/lib/solid-auth.js +2 -4
  96. package/dist/lib/solid-auth.js.map +1 -1
  97. package/dist/lib/solid-client-credentials-login.js +100 -0
  98. package/dist/lib/solid-client-credentials-login.js.map +1 -0
  99. package/dist/lib/solid-local-store.js +31 -0
  100. package/dist/lib/solid-local-store.js.map +1 -0
  101. package/dist/lib/symphony/archive.js +328 -18
  102. package/dist/lib/symphony/archive.js.map +1 -1
  103. package/dist/lib/symphony/pod-projection.js +2222 -0
  104. package/dist/lib/symphony/pod-projection.js.map +1 -0
  105. package/dist/lib/symphony-command.js +602 -178
  106. package/dist/lib/symphony-command.js.map +1 -1
  107. package/dist/lib/sync-checkpoint-store.js +74 -0
  108. package/dist/lib/sync-checkpoint-store.js.map +1 -0
  109. package/dist/skills/symphony/SKILL.md +665 -0
  110. package/package.json +15 -9
  111. package/vendor/agent-runtime/dist/agent-runtime.d.ts +137 -0
  112. package/vendor/agent-runtime/dist/agent-runtime.js +211 -0
  113. package/vendor/agent-runtime/dist/auto-mode.d.ts +78 -13
  114. package/vendor/agent-runtime/dist/auto-mode.js +288 -31
  115. package/vendor/agent-runtime/dist/control-plane.d.ts +28 -0
  116. package/vendor/agent-runtime/dist/control-plane.js +79 -0
  117. package/vendor/agent-runtime/dist/file-sync.d.ts +157 -0
  118. package/vendor/agent-runtime/dist/file-sync.js +314 -0
  119. package/vendor/agent-runtime/dist/index.d.ts +7 -0
  120. package/vendor/agent-runtime/dist/index.js +7 -0
  121. package/vendor/agent-runtime/dist/reconciler.d.ts +117 -0
  122. package/vendor/agent-runtime/dist/reconciler.js +361 -0
  123. package/vendor/agent-runtime/dist/symphony.d.ts +128 -8
  124. package/vendor/agent-runtime/dist/symphony.js +362 -57
  125. package/vendor/agent-runtime/dist/sync.d.ts +271 -0
  126. package/vendor/agent-runtime/dist/sync.js +550 -0
  127. package/vendor/agent-runtime/dist/thread-reconciler-controller.d.ts +58 -0
  128. package/vendor/agent-runtime/dist/thread-reconciler-controller.js +137 -0
  129. package/vendor/agent-runtime/dist/turn-controller.js +2 -2
  130. package/vendor/agent-runtime/dist/wake-scheduler.d.ts +67 -0
  131. package/vendor/agent-runtime/dist/wake-scheduler.js +194 -0
  132. package/vendor/agent-runtime/package.json +8 -1
  133. package/vendor/pi-web-access/CHANGELOG.md +387 -0
  134. package/vendor/pi-web-access/LICENSE +21 -0
  135. package/vendor/pi-web-access/README.md +352 -0
  136. package/vendor/pi-web-access/activity.ts +101 -0
  137. package/vendor/pi-web-access/banner.png +0 -0
  138. package/vendor/pi-web-access/chrome-cookies.ts +322 -0
  139. package/vendor/pi-web-access/code-search.ts +107 -0
  140. package/vendor/pi-web-access/curator-page.ts +3359 -0
  141. package/vendor/pi-web-access/curator-server.ts +605 -0
  142. package/vendor/pi-web-access/exa.ts +520 -0
  143. package/vendor/pi-web-access/extract.ts +641 -0
  144. package/vendor/pi-web-access/gemini-api.ts +112 -0
  145. package/vendor/pi-web-access/gemini-search.ts +361 -0
  146. package/vendor/pi-web-access/gemini-url-context.ts +126 -0
  147. package/vendor/pi-web-access/gemini-web-config.ts +52 -0
  148. package/vendor/pi-web-access/gemini-web.ts +396 -0
  149. package/vendor/pi-web-access/github-api.ts +196 -0
  150. package/vendor/pi-web-access/github-extract.ts +634 -0
  151. package/vendor/pi-web-access/index.ts +2346 -0
  152. package/vendor/pi-web-access/package.json +45 -0
  153. package/vendor/pi-web-access/pdf-extract.ts +192 -0
  154. package/vendor/pi-web-access/perplexity.ts +195 -0
  155. package/vendor/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  156. package/vendor/pi-web-access/rsc-extract.ts +338 -0
  157. package/vendor/pi-web-access/skills/librarian/SKILL.md +195 -0
  158. package/vendor/pi-web-access/storage.ts +72 -0
  159. package/vendor/pi-web-access/summary-review.ts +276 -0
  160. package/vendor/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +41 -0
  161. package/vendor/pi-web-access/test/pdf-extract.test.mjs +95 -0
  162. package/vendor/pi-web-access/utils.ts +44 -0
  163. package/vendor/pi-web-access/video-extract.ts +378 -0
  164. package/vendor/pi-web-access/youtube-extract.ts +310 -0
  165. package/dist/lib/pi-adapter/auth.js +0 -68
  166. package/dist/lib/pi-adapter/auth.js.map +0 -1
  167. package/dist/lib/pi-adapter/pod-tools.js +0 -140
  168. package/dist/lib/pi-adapter/pod-tools.js.map +0 -1
  169. package/dist/skills/drizzle-solid/SKILL.md +0 -340
  170. package/dist/skills/pod-storage/SKILL.md +0 -100
  171. package/dist/skills/solid-modeling/SKILL.md +0 -274
  172. package/dist/skills/xpod-componentsjs/SKILL.md +0 -284
@@ -1,218 +1,503 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { resolve } from 'node:path';
3
+ import { runThreadReconcilerCycle, summarizeWakeJobExecutionRecord, } from '../../vendor/agent-runtime/dist/index.js';
4
+ import { appendSymphonyReconcilerDecision, createRunPlan, renderSymphonyRuntimePrompt, } from '../../vendor/agent-runtime/dist/symphony.js';
3
5
  import { runAutoMode, listArchivedAutoModeSessions } from './auto-mode/index.js';
4
- import { createArchivedSymphonyRunPlan, formatSymphonyRecordSummary, getSymphonyHome, listSymphonyDeliveries, listSymphonyIssues, listSymphonySessions, resolveSymphonyRecord, updateSymphonyIssueStatus, updateSymphonyDeliveryStatus, updateSymphonySessionStatus, } from './symphony/archive.js';
5
- const SYMPHONY_BACKENDS = ['codex', 'claude', 'codebuddy'];
6
+ import { formatSymphonyRecordSummary, getSymphonyHome, attachSymphonyRunPlanToIssue, createSymphonyRunPlanDraft, triageSymphonyIssue, withSymphonyIssueStatus, withSymphonyDeliveryStatus, withSymphonySessionStatus, withSymphonyTaskStatus, writeSymphonyRunPlan, } from './symphony/archive.js';
7
+ import { listOpenSymphonyIssuesFromPod, mirrorSymphonyProjectionJsonLdFromPod, persistSymphonyProjectionToPod } from './symphony/pod-projection.js';
6
8
  const defaultRuntime = {
7
9
  runAutoMode,
8
10
  listAutoModeSessions: listArchivedAutoModeSessions,
11
+ persistSymphonyProjectionToPod,
12
+ listOpenSymphonyIssuesFromPod,
13
+ mirrorSymphonyProjectionJsonLdFromPod,
9
14
  };
10
- export function createSymphonyCommand(runtime = defaultRuntime) {
11
- return {
12
- command: 'symphony <command>',
13
- describe: 'Inspect and tune AI Secretary Symphony delegation',
14
- builder(command) {
15
- return buildSymphonyCommandTree(command, runtime)
16
- .demandCommand(1, 'Usage: linx symphony <run|issues|sessions|deliveries|show>');
15
+ export async function runSymphony(argv, runtime = defaultRuntime) {
16
+ throwIfAborted(argv.signal);
17
+ const objective = normalizeObjective(argv.objective);
18
+ const cwd = resolve(argv.cwd || process.cwd());
19
+ const workspace = resolveWorkspaceMetadata(cwd, argv);
20
+ const backend = argv.backend ?? 'codex';
21
+ const mode = 'off';
22
+ const secretaryAutoEnabled = Boolean(argv.auto);
23
+ const workers = normalizeWorkers(argv.worker, backend, argv.target);
24
+ const agentRuntime = resolveSymphonyAgentRuntime(argv);
25
+ const requestedWorkerModel = normalizeOptional(argv.workerModel) ?? normalizeOptional(argv.model);
26
+ assertSymphonyWorkerModelCompatibility(backend, requestedWorkerModel, workers);
27
+ const planInput = {
28
+ objective,
29
+ title: normalizeOptional(argv.title),
30
+ acceptanceCriteria: normalizeAcceptanceCriteria(argv.acceptance),
31
+ workspacePath: cwd,
32
+ workspaceKind: workspace.kind,
33
+ repository: workspace.repository,
34
+ branch: workspace.branch,
35
+ worktree: workspace.worktree,
36
+ baseRevision: workspace.baseRevision,
37
+ environment: {
38
+ kind: 'local-shell',
39
+ label: 'linx-cli',
40
+ runtime: backend,
17
41
  },
18
- handler() {
19
- // Subcommands own execution.
42
+ backend,
43
+ mode,
44
+ secretaryAutoEnabled,
45
+ model: normalizeOptional(argv.model),
46
+ workerModel: normalizeOptional(argv.workerModel),
47
+ workerSupervisorIntervalMs: normalizePositiveInteger(argv.workerSupervisorIntervalMs),
48
+ chat: normalizeOptional(argv.chat),
49
+ thread: normalizeOptional(argv.thread),
50
+ messages: normalizeMessages(argv.messages),
51
+ issuer: {
52
+ source: 'user',
53
+ chat: normalizeOptional(argv.chat),
54
+ thread: normalizeOptional(argv.thread),
55
+ messages: normalizeMessages(argv.messages),
20
56
  },
57
+ target: argv.target,
58
+ workers,
21
59
  };
60
+ const plan = await createSymphonyRunPlanForRuntime(planInput, runtime, argv.signal);
61
+ throwIfAborted(argv.signal);
62
+ const quietProjectionErrors = argv.quietProjectionErrors === true;
63
+ let currentPlan = await persistSymphonyProjectionBestEffort(plan, 'planned', runtime, {
64
+ quiet: quietProjectionErrors,
65
+ signal: argv.signal,
66
+ });
67
+ if (argv.dryRun) {
68
+ throwIfAborted(argv.signal);
69
+ currentPlan = withUpdatedIssue({
70
+ ...currentPlan,
71
+ workers: currentPlan.workers.map((worker) => ({
72
+ ...worker,
73
+ session: withSymphonySessionStatus(worker.session, 'planned', { dryRun: true }),
74
+ })),
75
+ });
76
+ currentPlan = await persistSymphonyProjectionBestEffort(currentPlan, 'planned', runtime, {
77
+ quiet: quietProjectionErrors,
78
+ signal: argv.signal,
79
+ });
80
+ if (argv.print !== false) {
81
+ printSymphonyRunPlan(currentPlan, { dryRun: true });
82
+ }
83
+ return currentPlan;
84
+ }
85
+ let issue = withSymphonyIssueStatus(currentPlan.issue, 'in_progress');
86
+ currentPlan = { ...currentPlan, issue };
87
+ currentPlan = await persistSymphonyProjectionBestEffort(currentPlan, 'running', runtime, {
88
+ quiet: quietProjectionErrors,
89
+ signal: argv.signal,
90
+ });
91
+ issue = currentPlan.issue;
92
+ try {
93
+ const dispatchedWorkers = [];
94
+ let firstFailure = null;
95
+ for (const [index, worker] of currentPlan.workers.entries()) {
96
+ throwIfAborted(argv.signal);
97
+ const dispatched = await runSymphonyWorker({
98
+ worker,
99
+ issue: currentPlan.issue,
100
+ workerIndex: index + 1,
101
+ workerCount: currentPlan.workers.length,
102
+ secretaryAutoEnabled,
103
+ cwd,
104
+ plain: Boolean(argv.plain),
105
+ agentRuntime,
106
+ model: normalizeOptional(argv.workerModel) ?? normalizeOptional(argv.model),
107
+ credentialSource: normalizeCredentialSource(argv.credentialSource),
108
+ goalMode: argv.workerGoalMode !== false,
109
+ quiet: argv.quietWorkers === true,
110
+ passthroughArgs: (argv['--'] ?? []).map(String),
111
+ commandOverride: normalizeOptional(argv.commandOverride),
112
+ commandEnv: normalizeCommandEnv(argv.commandEnv),
113
+ runtime,
114
+ signal: argv.signal,
115
+ onWorkerDispatched: async (runningWorker) => {
116
+ currentPlan = withUpdatedWorker(currentPlan, runningWorker);
117
+ currentPlan = await persistSymphonyProjectionBestEffort(currentPlan, 'running', runtime, {
118
+ quiet: quietProjectionErrors,
119
+ signal: argv.signal,
120
+ });
121
+ },
122
+ });
123
+ dispatchedWorkers.push(dispatched.worker);
124
+ currentPlan = withUpdatedWorker(currentPlan, dispatched.worker);
125
+ if (dispatched.exitCode !== 0 && !firstFailure) {
126
+ firstFailure = {
127
+ exitCode: dispatched.exitCode,
128
+ error: `Backend ${dispatched.worker.session.backend} exited with code ${dispatched.exitCode}`,
129
+ };
130
+ }
131
+ }
132
+ const status = firstFailure ? 'failed' : 'completed';
133
+ issue = withSymphonyIssueStatus(issue, firstFailure ? 'blocked' : 'resolved', firstFailure ? { error: firstFailure.error } : {});
134
+ currentPlan = withUpdatedIssue({
135
+ ...currentPlan,
136
+ issue,
137
+ workers: dispatchedWorkers,
138
+ });
139
+ currentPlan = await persistSymphonyProjectionBestEffort(currentPlan, status, runtime, {
140
+ quiet: quietProjectionErrors,
141
+ signal: argv.signal,
142
+ });
143
+ if (argv.print !== false) {
144
+ printSymphonyRunPlan(currentPlan, { dryRun: false });
145
+ }
146
+ if (firstFailure) {
147
+ process.exitCode = firstFailure.exitCode;
148
+ }
149
+ return currentPlan;
150
+ }
151
+ catch (error) {
152
+ if (isAbortError(error)) {
153
+ throw error;
154
+ }
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ issue = withSymphonyIssueStatus(issue, 'blocked', { error: message });
157
+ const failedWorker = currentPlan.workers[0];
158
+ if (failedWorker) {
159
+ const worker = {
160
+ task: failedWorker.task,
161
+ taskRecord: withSymphonyTaskStatus(failedWorker.taskRecord, 'failed', { error: message }),
162
+ delivery: withSymphonyDeliveryStatus(failedWorker.delivery, 'failed', { error: message }),
163
+ session: withSymphonySessionStatus(failedWorker.session, 'failed', { error: message, exitCode: 1 }),
164
+ };
165
+ currentPlan = withUpdatedWorker({ ...currentPlan, issue }, worker);
166
+ }
167
+ else {
168
+ currentPlan = { ...currentPlan, issue };
169
+ }
170
+ await persistSymphonyProjectionBestEffort(currentPlan, 'failed', runtime, {
171
+ quiet: quietProjectionErrors,
172
+ signal: argv.signal,
173
+ });
174
+ throw error;
175
+ }
22
176
  }
23
- export const symphonyCommand = createSymphonyCommand();
24
- export function buildSymphonyCommandTree(command, runtime = defaultRuntime) {
25
- return command
26
- .command(createRunCommand(runtime))
27
- .command(createListCommand('issues', 'List Symphony issues', () => listSymphonyIssues().map((record) => formatSymphonyRecordSummary('issue', record))))
28
- .command(createListCommand('sessions', 'List Symphony sessions', () => listSymphonySessions().map((record) => formatSymphonyRecordSummary('session', record))))
29
- .command(createListCommand('deliveries', 'List Symphony deliveries', () => listSymphonyDeliveries().map((record) => formatSymphonyRecordSummary('delivery', record))))
30
- .command(createShowCommand());
177
+ async function createSymphonyRunPlanForRuntime(input, runtime, signal) {
178
+ throwIfAborted(signal);
179
+ const listIssues = runtime.listOpenSymphonyIssuesFromPod;
180
+ if (!listIssues) {
181
+ return createSymphonyRunPlanDraft(input);
182
+ }
183
+ let podIssues;
184
+ try {
185
+ podIssues = await withAbortSignal(listIssues(), signal);
186
+ throwIfAborted(signal);
187
+ }
188
+ catch (error) {
189
+ if (isAbortError(error)) {
190
+ throw error;
191
+ }
192
+ podIssues = null;
193
+ }
194
+ if (!podIssues) {
195
+ return createSymphonyRunPlanDraft(input);
196
+ }
197
+ const plan = createRunPlan(input);
198
+ const triage = triageSymphonyIssue({
199
+ objective: input.objective,
200
+ chat: input.chat,
201
+ thread: input.thread,
202
+ workspacePath: input.workspacePath,
203
+ issues: podIssues,
204
+ });
205
+ return triage.action === 'update' && triage.issue
206
+ ? attachSymphonyRunPlanToIssue(plan, triage.issue)
207
+ : plan;
31
208
  }
32
- function createRunCommand(runtime) {
33
- return {
34
- command: 'run [objective..]',
35
- describe: 'Ask AI Secretary to delegate work through Symphony',
36
- builder(command) {
37
- return command
38
- .positional('objective', {
39
- array: true,
40
- type: 'string',
41
- describe: 'Task objective for Secretary to delegate',
42
- })
43
- .option('backend', {
44
- type: 'string',
45
- choices: SYMPHONY_BACKENDS,
46
- default: 'codex',
47
- describe: 'Worker backend receiving Secretary-projected work',
48
- })
49
- .option('auto', {
50
- type: 'boolean',
51
- default: false,
52
- describe: 'Let AI Secretary handle in-policy worker confirmations',
53
- })
54
- .option('dry-run', {
55
- type: 'boolean',
56
- default: false,
57
- describe: 'Archive the issue/delivery/session plan without launching a backend',
58
- })
59
- .option('cwd', {
60
- type: 'string',
61
- describe: 'Workspace path for the target runtime session',
62
- })
63
- .option('title', {
64
- type: 'string',
65
- describe: 'Human-readable task title',
66
- })
67
- .option('acceptance', {
68
- alias: 'a',
69
- array: true,
70
- type: 'string',
71
- describe: 'Acceptance criterion; repeat for multiple criteria',
72
- })
73
- .option('model', {
74
- type: 'string',
75
- describe: 'Model id forwarded to the backend',
76
- })
77
- .option('plain', {
78
- type: 'boolean',
79
- default: false,
80
- describe: 'Disable full-screen backend UI and use plain output',
81
- })
82
- .option('repository', {
83
- type: 'string',
84
- describe: 'Repository URL metadata override',
85
- })
86
- .option('branch', {
87
- type: 'string',
88
- describe: 'Git branch metadata override',
89
- })
90
- .option('worktree', {
91
- type: 'string',
92
- describe: 'Git worktree metadata override',
93
- })
94
- .option('workspace-kind', {
95
- type: 'string',
96
- choices: ['git', 'folder'],
97
- describe: 'Workspace kind metadata override',
209
+ async function runSymphonyWorker(input) {
210
+ throwIfAborted(input.signal);
211
+ const beforeAutoModeIds = new Set(input.runtime.listAutoModeSessions().map((record) => record.id));
212
+ const cwd = input.worker.session.cwd || input.cwd;
213
+ const workspace = input.worker.session.workspace ?? {
214
+ path: cwd,
215
+ kind: 'folder',
216
+ };
217
+ const prompt = renderSymphonyRuntimePrompt({
218
+ issue: input.issue,
219
+ task: input.worker.task,
220
+ objective: input.worker.taskRecord.objective,
221
+ acceptanceCriteria: input.worker.taskRecord.acceptanceCriteria,
222
+ workspace,
223
+ backend: input.worker.session.backend,
224
+ mode: input.worker.session.mode,
225
+ secretaryAutoEnabled: input.worker.session.secretaryAutoEnabled ?? input.secretaryAutoEnabled,
226
+ session: input.worker.session.uri,
227
+ target: input.worker.session.target,
228
+ issuer: input.issue.issuer,
229
+ ...(input.workerCount > 1 ? {
230
+ workerIndex: input.workerIndex,
231
+ workerCount: input.workerCount,
232
+ } : {}),
233
+ });
234
+ let exitCode = null;
235
+ let wakeError;
236
+ let runningDelivery = input.worker.delivery;
237
+ let runningSession = input.worker.session;
238
+ let runningTask = input.worker.taskRecord;
239
+ const cycle = await runThreadReconcilerCycle({
240
+ policy: {
241
+ kind: 'symphony',
242
+ assignedWorkerAgent: input.worker.delivery.targetAgent,
243
+ secretaryAgent: '__secretary__',
244
+ },
245
+ handleWakeJob: async ({ decisionSummary, record }) => {
246
+ try {
247
+ throwIfAborted(input.signal);
248
+ exitCode = await input.runtime.runAutoMode({
249
+ backend: input.worker.session.backend,
250
+ autoEnabled: input.secretaryAutoEnabled,
251
+ mode: 'off',
252
+ cwd,
253
+ plain: input.plain,
254
+ model: normalizeOptional(input.worker.session.model) ?? input.model,
255
+ credentialSource: input.credentialSource,
256
+ prompt,
257
+ goalMode: input.goalMode,
258
+ quiet: input.quiet,
259
+ passthroughArgs: input.passthroughArgs,
260
+ ...(input.commandOverride ? { commandOverride: input.commandOverride } : {}),
261
+ ...(input.commandEnv ? { commandEnv: input.commandEnv } : {}),
262
+ metadata: {
263
+ symphony: {
264
+ issue: input.issue.uri,
265
+ task: input.worker.task,
266
+ delivery: input.worker.delivery.uri,
267
+ session: input.worker.session.uri,
268
+ ...(input.agentRuntime ? { agentRuntime: input.agentRuntime } : {}),
269
+ ...(input.worker.session.model ? { workerModel: input.worker.session.model } : {}),
270
+ ...(input.worker.session.supervisor ? { supervisor: input.worker.session.supervisor } : {}),
271
+ },
272
+ reconciler: decisionSummary,
273
+ scheduler: {
274
+ wakeRecord: summarizeWakeJobExecutionRecord(record),
275
+ },
276
+ },
277
+ ...(input.signal ? { signal: input.signal } : {}),
278
+ });
279
+ throwIfAborted(input.signal);
280
+ return { exitCode };
281
+ }
282
+ catch (error) {
283
+ wakeError = error;
284
+ throw error;
285
+ }
286
+ },
287
+ event: createSymphonyWorkerDispatchEvent(input.worker),
288
+ dispatchOptions: {
289
+ randomId: `${input.worker.delivery.uri}-dispatch`,
290
+ },
291
+ onDispatched: (dispatch) => {
292
+ throwIfAborted(input.signal);
293
+ runningDelivery = appendSymphonyReconcilerDecision(withSymphonyDeliveryStatus(input.worker.delivery, 'dispatched'), dispatch.summary);
294
+ runningSession = appendSymphonyReconcilerDecision(withSymphonySessionStatus(input.worker.session, 'running'), dispatch.summary);
295
+ runningTask = appendSymphonyReconcilerDecision(withSymphonyTaskStatus(input.worker.taskRecord, 'running'), dispatch.summary);
296
+ return input.onWorkerDispatched?.({
297
+ task: input.worker.task,
298
+ taskRecord: runningTask,
299
+ delivery: runningDelivery,
300
+ session: runningSession,
98
301
  });
99
302
  },
100
- async handler(argv) {
101
- await runSymphony(argv, runtime);
303
+ });
304
+ const dispatchScheduler = cycle.schedulerSummary;
305
+ if (dispatchScheduler.failed.length > 0) {
306
+ throw wakeError ?? new Error(String(dispatchScheduler.failed[0]?.error ?? 'Symphony worker wake job failed'));
307
+ }
308
+ if (exitCode === null) {
309
+ throw new Error('Symphony worker was not awakened by the Thread Reconciler.');
310
+ }
311
+ throwIfAborted(input.signal);
312
+ const autoModeSessionId = resolveCreatedAutoModeSessionId(beforeAutoModeIds, input.runtime);
313
+ const status = exitCode === 0 ? 'completed' : 'failed';
314
+ const statusDecision = await dispatchSymphonyWorkerStatusDecision({
315
+ worker: {
316
+ task: input.worker.task,
317
+ taskRecord: runningTask,
318
+ delivery: runningDelivery,
319
+ session: runningSession,
320
+ },
321
+ status,
322
+ exitCode,
323
+ autoModeSessionId,
324
+ });
325
+ return {
326
+ status,
327
+ exitCode,
328
+ worker: {
329
+ task: input.worker.task,
330
+ taskRecord: appendSymphonyReconcilerDecision(withSymphonyTaskStatus(runningTask, status, {
331
+ ...(exitCode === 0 ? {} : { error: `Backend exited with code ${exitCode}` }),
332
+ }), statusDecision.decision),
333
+ delivery: appendSymphonyReconcilerDecision(withSymphonyDeliveryStatus(runningDelivery, status, {
334
+ autoModeSessionId,
335
+ ...(exitCode === 0 ? {} : { error: `Backend exited with code ${exitCode}` }),
336
+ }), statusDecision.decision),
337
+ session: appendSymphonyReconcilerDecision(withSymphonySessionStatus(runningSession, status, {
338
+ autoModeSessionId,
339
+ exitCode,
340
+ ...(exitCode === 0 ? {} : { error: `Backend exited with code ${exitCode}` }),
341
+ }), statusDecision.decision),
102
342
  },
103
343
  };
104
344
  }
105
- function createListCommand(commandName, describe, loadLines) {
345
+ function createSymphonyWorkerDispatchEvent(worker) {
106
346
  return {
107
- command: commandName,
108
- describe,
109
- builder(command) {
110
- return command;
347
+ type: 'delivery.submitted',
348
+ ...(worker.delivery.chat ? { chat: worker.delivery.chat } : {}),
349
+ ...(worker.delivery.thread ? { thread: worker.delivery.thread } : {}),
350
+ resource: worker.delivery.uri,
351
+ actor: {
352
+ id: '__secretary__',
353
+ role: 'secretary',
354
+ },
355
+ data: {
356
+ deliveryType: worker.delivery.type,
357
+ issue: worker.delivery.issue,
358
+ task: worker.delivery.task,
359
+ delivery: worker.delivery.uri,
360
+ session: worker.session.uri,
361
+ },
362
+ };
363
+ }
364
+ async function dispatchSymphonyWorkerStatusDecision(input) {
365
+ let wakeError;
366
+ const cycle = await runThreadReconcilerCycle({
367
+ policy: {
368
+ kind: 'symphony',
369
+ secretaryAgent: '__secretary__',
111
370
  },
112
- handler() {
113
- const lines = loadLines();
114
- if (lines.length === 0) {
115
- process.stdout.write(`No Symphony ${commandName} found.\n`);
116
- return;
371
+ handleWakeJob: ({ job }) => {
372
+ try {
373
+ return {
374
+ targetAgent: job.targetAgent,
375
+ targetRole: job.targetRole,
376
+ status: input.status,
377
+ };
378
+ }
379
+ catch (error) {
380
+ wakeError = error;
381
+ throw error;
117
382
  }
118
- process.stdout.write(`${lines.join('\n')}\n`);
119
383
  },
384
+ event: createSymphonyWorkerStatusEvent(input),
385
+ dispatchOptions: {
386
+ randomId: `${input.worker.delivery.uri}-${input.status}`,
387
+ },
388
+ });
389
+ const scheduler = cycle.schedulerSummary;
390
+ if (scheduler.failed.length > 0) {
391
+ throw wakeError ?? new Error(String(scheduler.failed[0]?.error ?? 'Symphony status wake job failed'));
392
+ }
393
+ return {
394
+ decision: cycle.summary,
395
+ scheduler,
120
396
  };
121
397
  }
122
- function createShowCommand() {
398
+ function createSymphonyWorkerStatusEvent(input) {
123
399
  return {
124
- command: 'show <id>',
125
- describe: 'Show a Symphony issue, delivery, or session by URI/key prefix',
126
- builder(command) {
127
- return command.positional('id', {
128
- type: 'string',
129
- describe: 'Issue, delivery, or session URI/key prefix',
130
- });
400
+ type: input.status === 'completed' ? 'delivery.completed' : 'delivery.failed',
401
+ ...(input.worker.delivery.chat ? { chat: input.worker.delivery.chat } : {}),
402
+ ...(input.worker.delivery.thread ? { thread: input.worker.delivery.thread } : {}),
403
+ resource: input.worker.delivery.uri,
404
+ actor: {
405
+ id: input.worker.delivery.targetAgent,
406
+ role: 'worker',
131
407
  },
132
- handler(argv) {
133
- const id = typeof argv.id === 'string' ? argv.id : '';
134
- const resolved = resolveSymphonyRecord(id);
135
- if (!resolved) {
136
- throw new Error(`Symphony record not found: ${id}`);
137
- }
138
- process.stdout.write(`${formatSymphonyRecordSummary(resolved.kind, resolved.record)}\n`);
139
- process.stdout.write(`${JSON.stringify(resolved.record, null, 2)}\n`);
408
+ data: {
409
+ issue: input.worker.delivery.issue,
410
+ task: input.worker.delivery.task,
411
+ delivery: input.worker.delivery.uri,
412
+ session: input.worker.session.uri,
413
+ autoModeSessionId: input.autoModeSessionId,
414
+ exitCode: input.exitCode,
140
415
  },
141
416
  };
142
417
  }
143
- export async function runSymphony(argv, runtime = defaultRuntime) {
144
- const objective = normalizeObjective(argv.objective);
145
- const cwd = resolve(argv.cwd || process.cwd());
146
- const workspace = resolveWorkspaceMetadata(cwd, argv);
147
- const backend = argv.backend ?? 'codex';
148
- const mode = argv.auto ? 'auto' : 'manual';
149
- const plan = createArchivedSymphonyRunPlan({
150
- objective,
151
- title: normalizeOptional(argv.title),
152
- acceptanceCriteria: normalizeAcceptanceCriteria(argv.acceptance),
153
- workspacePath: cwd,
154
- workspaceKind: workspace.kind,
155
- repository: workspace.repository,
156
- branch: workspace.branch,
157
- worktree: workspace.worktree,
158
- backend,
159
- mode,
160
- model: normalizeOptional(argv.model),
161
- });
162
- if (argv.dryRun) {
163
- updateSymphonySessionStatus(plan.session, 'planned', { dryRun: true });
164
- printSymphonyRunPlan(plan, { dryRun: true });
418
+ async function persistSymphonyProjectionBestEffort(plan, stage, runtime, options = {}) {
419
+ throwIfAborted(options.signal);
420
+ const persist = runtime.persistSymphonyProjectionToPod;
421
+ if (!persist) {
422
+ writeSymphonyRunPlan(plan);
165
423
  return plan;
166
424
  }
167
- let issue = updateSymphonyIssueStatus(plan.issue, 'in_progress');
168
- let delivery = updateSymphonyDeliveryStatus(plan.delivery, 'dispatched');
169
- let session = updateSymphonySessionStatus(plan.session, 'running');
170
- const beforeAutoModeIds = new Set(runtime.listAutoModeSessions().map((record) => record.id));
171
425
  try {
172
- const exitCode = await runtime.runAutoMode({
173
- backend,
174
- mode,
175
- autoModeEnabled: argv.auto,
176
- cwd,
177
- plain: Boolean(argv.plain),
178
- model: normalizeOptional(argv.model),
179
- prompt: plan.delivery.projection.prompt,
180
- goalMode: true,
181
- passthroughArgs: (argv['--'] ?? []).map(String),
182
- });
183
- const autoModeSessionId = resolveCreatedAutoModeSessionId(beforeAutoModeIds, runtime);
184
- const status = exitCode === 0 ? 'completed' : 'failed';
185
- issue = updateSymphonyIssueStatus(issue, exitCode === 0 ? 'resolved' : 'blocked', exitCode === 0 ? {} : { error: `Backend exited with code ${exitCode}` });
186
- delivery = updateSymphonyDeliveryStatus(delivery, status, {
187
- autoModeSessionId,
188
- ...(exitCode === 0 ? {} : { error: `Backend exited with code ${exitCode}` }),
189
- });
190
- session = updateSymphonySessionStatus(session, status, {
191
- autoModeSessionId,
192
- exitCode,
193
- ...(exitCode === 0 ? {} : { error: `Backend exited with code ${exitCode}` }),
194
- });
195
- printSymphonyRunPlan({ issue, task: plan.task, delivery, session }, { dryRun: false });
196
- if (exitCode !== 0) {
197
- process.exitCode = exitCode;
426
+ const result = await withAbortSignal(persist(plan, { stage }), options.signal);
427
+ throwIfAborted(options.signal);
428
+ if (!result) {
429
+ writeSymphonyRunPlan(plan);
430
+ return plan;
198
431
  }
199
- return { issue, task: plan.task, delivery, session };
432
+ await mirrorSymphonyProjectionBestEffort(result, runtime, options);
433
+ return result.plan;
200
434
  }
201
435
  catch (error) {
436
+ if (isAbortError(error)) {
437
+ throw error;
438
+ }
202
439
  const message = error instanceof Error ? error.message : String(error);
203
- updateSymphonyIssueStatus(issue, 'blocked', { error: message });
204
- updateSymphonyDeliveryStatus(delivery, 'failed', { error: message });
205
- updateSymphonySessionStatus(session, 'failed', { error: message, exitCode: 1 });
206
- throw error;
440
+ if (!options.quiet) {
441
+ process.stderr.write(`[symphony] Pod projection skipped: ${message}\n`);
442
+ }
443
+ writeSymphonyRunPlan(plan);
444
+ return plan;
445
+ }
446
+ }
447
+ async function mirrorSymphonyProjectionBestEffort(result, runtime, options = {}) {
448
+ throwIfAborted(options.signal);
449
+ if (!result) {
450
+ return;
451
+ }
452
+ const mirror = runtime.mirrorSymphonyProjectionJsonLdFromPod;
453
+ if (!mirror) {
454
+ return;
455
+ }
456
+ try {
457
+ await withAbortSignal(mirror(result), options.signal);
458
+ }
459
+ catch (error) {
460
+ if (isAbortError(error)) {
461
+ throw error;
462
+ }
463
+ const message = error instanceof Error ? error.message : String(error);
464
+ if (!options.quiet) {
465
+ process.stderr.write(`[symphony] Pod JSON-LD mirror skipped: ${message}\n`);
466
+ }
207
467
  }
208
468
  }
469
+ function withUpdatedIssue(plan) {
470
+ const primary = plan.workers[0];
471
+ if (!primary) {
472
+ return plan;
473
+ }
474
+ return {
475
+ ...plan,
476
+ task: primary.task,
477
+ delivery: primary.delivery,
478
+ session: primary.session,
479
+ };
480
+ }
481
+ function withUpdatedWorker(plan, worker) {
482
+ const workers = plan.workers.map((candidate) => (candidate.session.uri === worker.session.uri ? worker : candidate));
483
+ return withUpdatedIssue({
484
+ ...plan,
485
+ workers,
486
+ });
487
+ }
209
488
  function printSymphonyRunPlan(plan, options) {
210
- process.stdout.write(options.dryRun ? 'LinX Symphony dry-run\n' : 'LinX Symphony run\n');
211
- process.stdout.write(`issue: ${formatSymphonyRecordSummary('issue', plan.issue)}\n`);
212
- process.stdout.write(`task: ${plan.task}\n`);
213
- process.stdout.write(`delivery: ${formatSymphonyRecordSummary('delivery', plan.delivery)}\n`);
214
- process.stdout.write(`session: ${formatSymphonyRecordSummary('session', plan.session)}\n`);
215
- process.stdout.write(`archive: ${getSymphonyHome()}\n`);
489
+ process.stdout.write(options.dryRun ? 'LinX Secretary Symphony dry-run\n' : 'LinX Secretary Symphony dispatch\n');
490
+ process.stdout.write(`work: ${formatSymphonyRecordSummary('issue', plan.issue)}\n`);
491
+ for (const [index, worker] of plan.workers.entries()) {
492
+ const prefix = plan.workers.length > 1 ? `worker ${index + 1}/${plan.workers.length}` : 'worker';
493
+ if (worker.taskRecord) {
494
+ process.stdout.write(`${prefix}: ${formatSymphonyRecordSummary('task', worker.taskRecord)}\n`);
495
+ }
496
+ process.stdout.write(`task: ${worker.task}\n`);
497
+ process.stdout.write(`dispatch: ${formatSymphonyRecordSummary('delivery', worker.delivery)}\n`);
498
+ process.stdout.write(`session: ${formatSymphonyRecordSummary('session', worker.session)}\n`);
499
+ }
500
+ process.stdout.write(`local mirror/cache: ${getSymphonyHome()}\n`);
216
501
  if (options.dryRun) {
217
502
  process.stdout.write('\nProjected runtime prompt:\n');
218
503
  process.stdout.write(`${plan.delivery.projection.prompt}\n`);
@@ -224,6 +509,52 @@ function resolveCreatedAutoModeSessionId(beforeIds, runtime) {
224
509
  .sort((left, right) => right.startedAt.localeCompare(left.startedAt));
225
510
  return created[0]?.id;
226
511
  }
512
+ function assertSymphonyWorkerModelCompatibility(fallbackBackend, workerModel, workers) {
513
+ if (!workerModel || !isNonCodexProviderModel(workerModel)) {
514
+ return;
515
+ }
516
+ const targets = workers && workers.length > 0 ? workers : [{ backend: fallbackBackend }];
517
+ const codexTarget = targets.find((target) => (target.backend ?? fallbackBackend) === 'codex');
518
+ if (codexTarget) {
519
+ throw new Error(`codex backend cannot run worker model ${workerModel}. Use backend claude/cc or linx for provider-routed models.`);
520
+ }
521
+ const claudeTarget = targets.find((target) => (target.backend ?? fallbackBackend) === 'claude');
522
+ if (claudeTarget && isProviderRoutedClaudeModel(workerModel)) {
523
+ throw new Error(`claude backend cannot set provider-routed worker model ${workerModel}. Configure it behind a Claude Code alias such as opus, or omit the worker model.`);
524
+ }
525
+ }
526
+ function isNonCodexProviderModel(model) {
527
+ return /(?:deepseek|claude|qwen|gemini|kimi|moonshot|mistral|grok|glm|minimax)/iu.test(model);
528
+ }
529
+ function isProviderRoutedClaudeModel(model) {
530
+ return /(?:deepseek|qwen|gemini|kimi|moonshot|mistral|grok|glm|minimax)/iu.test(model);
531
+ }
532
+ function normalizeWorkers(values, fallbackBackend, explicitTarget) {
533
+ if (!values || values.length === 0) {
534
+ return explicitTarget ? [explicitTarget] : undefined;
535
+ }
536
+ return values.map((value) => {
537
+ const [backendPart, agentPart] = value.split(':', 2);
538
+ const backend = normalizeBackend(backendPart) ?? fallbackBackend;
539
+ const agent = normalizeOptional(agentPart) ?? `${backend}-worker`;
540
+ return {
541
+ source: 'explicit-backend',
542
+ backend,
543
+ agent,
544
+ label: agent,
545
+ };
546
+ });
547
+ }
548
+ function normalizeBackend(value) {
549
+ const normalized = normalizeOptional(value);
550
+ if (normalized === 'cc') {
551
+ return 'claude';
552
+ }
553
+ if (normalized === 'linx' || normalized === 'codex' || normalized === 'claude' || normalized === 'codebuddy') {
554
+ return normalized;
555
+ }
556
+ return undefined;
557
+ }
227
558
  function normalizeObjective(parts) {
228
559
  const objective = (parts ?? [])
229
560
  .filter((item) => typeof item === 'string')
@@ -231,7 +562,7 @@ function normalizeObjective(parts) {
231
562
  .replace(/\s+/g, ' ')
232
563
  .trim();
233
564
  if (!objective) {
234
- throw new Error('Usage: linx symphony run <objective> [--backend codex] [--dry-run]');
565
+ throw new Error('Symphony objective is required');
235
566
  }
236
567
  return objective;
237
568
  }
@@ -242,10 +573,102 @@ function normalizeAcceptanceCriteria(value) {
242
573
  .map((item) => item.trim())
243
574
  .filter(Boolean);
244
575
  }
576
+ function normalizeMessages(value) {
577
+ const messages = (value ?? [])
578
+ .map((item) => normalizeOptional(item))
579
+ .filter((item) => Boolean(item));
580
+ return messages.length > 0 ? messages : undefined;
581
+ }
582
+ function resolveSymphonyAgentRuntime(argv) {
583
+ const explicit = normalizeAgentRuntimeConfig(argv.agentRuntime);
584
+ const model = explicit?.model
585
+ ?? normalizeOptional(argv.secretaryModel)
586
+ ?? normalizeOptional(argv.model);
587
+ if (!explicit && !model) {
588
+ return undefined;
589
+ }
590
+ return {
591
+ backend: explicit?.backend ?? 'linx',
592
+ credentialSource: explicit?.credentialSource ?? 'cloud',
593
+ ...explicit,
594
+ ...(model ? { model } : {}),
595
+ };
596
+ }
597
+ function normalizeAgentRuntimeConfig(value) {
598
+ if (!value) {
599
+ return undefined;
600
+ }
601
+ const metadata = isRecord(value.metadata) ? { ...value.metadata } : undefined;
602
+ const resolved = {
603
+ ...(normalizeOptional(value.backend) ? { backend: normalizeOptional(value.backend) } : {}),
604
+ ...(normalizeOptional(value.model) ? { model: normalizeOptional(value.model) } : {}),
605
+ ...(normalizeOptional(value.credentialSource) ? { credentialSource: normalizeOptional(value.credentialSource) } : {}),
606
+ ...(normalizeOptional(value.runtime) ? { runtime: normalizeOptional(value.runtime) } : {}),
607
+ ...(normalizeOptional(value.transport) ? { transport: normalizeOptional(value.transport) } : {}),
608
+ ...(normalizeOptional(value.endpoint) ? { endpoint: normalizeOptional(value.endpoint) } : {}),
609
+ ...(metadata ? { metadata } : {}),
610
+ };
611
+ return Object.keys(resolved).length > 0 ? resolved : undefined;
612
+ }
245
613
  function normalizeOptional(value) {
246
614
  const normalized = typeof value === 'string' ? value.trim() : '';
247
615
  return normalized || undefined;
248
616
  }
617
+ function isRecord(value) {
618
+ return typeof value === 'object' && value !== null;
619
+ }
620
+ function normalizeCommandEnv(value) {
621
+ if (!value) {
622
+ return undefined;
623
+ }
624
+ const entries = Object.entries(value)
625
+ .map(([key, entryValue]) => [key.trim(), String(entryValue)])
626
+ .filter(([key]) => Boolean(key));
627
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
628
+ }
629
+ function normalizeCredentialSource(value) {
630
+ return value === 'local' ? 'local' : value === 'cloud' ? 'cloud' : undefined;
631
+ }
632
+ function normalizePositiveInteger(value) {
633
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
634
+ return undefined;
635
+ }
636
+ const normalized = Math.trunc(value);
637
+ return normalized > 0 ? normalized : undefined;
638
+ }
639
+ function createAbortError(message = 'The operation was aborted.') {
640
+ const error = new Error(message);
641
+ error.name = 'AbortError';
642
+ return error;
643
+ }
644
+ function isAbortError(error) {
645
+ return error instanceof Error && (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted'));
646
+ }
647
+ function throwIfAborted(signal) {
648
+ if (!signal?.aborted) {
649
+ return;
650
+ }
651
+ const reason = signal.reason;
652
+ if (reason instanceof Error) {
653
+ throw reason;
654
+ }
655
+ throw createAbortError(typeof reason === 'string' && reason.trim() ? reason : undefined);
656
+ }
657
+ function withAbortSignal(promise, signal) {
658
+ if (!signal) {
659
+ return promise;
660
+ }
661
+ throwIfAborted(signal);
662
+ return new Promise((resolve, reject) => {
663
+ const onAbort = () => reject(signal.reason instanceof Error
664
+ ? signal.reason
665
+ : createAbortError(typeof signal.reason === 'string' && signal.reason.trim() ? signal.reason : undefined));
666
+ signal.addEventListener('abort', onAbort, { once: true });
667
+ promise
668
+ .then(resolve, reject)
669
+ .finally(() => signal.removeEventListener('abort', onAbort));
670
+ });
671
+ }
249
672
  function resolveWorkspaceMetadata(cwd, argv) {
250
673
  const git = isGitWorkspace(cwd);
251
674
  return {
@@ -253,6 +676,7 @@ function resolveWorkspaceMetadata(cwd, argv) {
253
676
  repository: normalizeOptional(argv.repository) ?? (git ? gitOutput(cwd, ['remote', 'get-url', 'origin']) : undefined),
254
677
  branch: normalizeOptional(argv.branch) ?? (git ? gitOutput(cwd, ['branch', '--show-current']) : undefined),
255
678
  worktree: normalizeOptional(argv.worktree) ?? (git ? gitOutput(cwd, ['rev-parse', '--show-toplevel']) : undefined),
679
+ baseRevision: git ? gitOutput(cwd, ['rev-parse', 'HEAD']) : undefined,
256
680
  };
257
681
  }
258
682
  function isGitWorkspace(cwd) {