screenhand 0.1.1 → 0.3.0

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 (241) hide show
  1. package/README.md +193 -109
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +5876 -0
  4. package/dist/scripts/codex-monitor-daemon.js +335 -0
  5. package/dist/scripts/export-help-center.js +112 -0
  6. package/dist/scripts/marketing-loop.js +117 -0
  7. package/dist/scripts/observer-daemon.js +288 -0
  8. package/dist/scripts/orchestrator-daemon.js +399 -0
  9. package/dist/scripts/supervisor-daemon.js +272 -0
  10. package/dist/scripts/threads-campaign.js +208 -0
  11. package/dist/scripts/worker-daemon.js +228 -0
  12. package/dist/src/agent/cli.js +82 -0
  13. package/dist/src/agent/loop.js +274 -0
  14. package/dist/src/community/fetcher.js +109 -0
  15. package/dist/src/community/index.js +6 -0
  16. package/dist/src/community/publisher.js +191 -0
  17. package/dist/src/community/remote-api.js +121 -0
  18. package/dist/src/community/types.js +3 -0
  19. package/dist/src/community/validator.js +95 -0
  20. package/{src/config.ts → dist/src/config.js} +5 -10
  21. package/dist/src/context-tracker.js +489 -0
  22. package/{src/index.ts → dist/src/index.js} +32 -52
  23. package/dist/src/ingestion/coverage-auditor.js +233 -0
  24. package/dist/src/ingestion/doc-parser.js +164 -0
  25. package/dist/src/ingestion/index.js +8 -0
  26. package/dist/src/ingestion/menu-scanner.js +152 -0
  27. package/dist/src/ingestion/reference-merger.js +186 -0
  28. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  29. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  30. package/dist/src/ingestion/types.js +3 -0
  31. package/dist/src/jobs/manager.js +305 -0
  32. package/dist/src/jobs/runner.js +806 -0
  33. package/dist/src/jobs/store.js +102 -0
  34. package/dist/src/jobs/types.js +30 -0
  35. package/dist/src/jobs/worker.js +97 -0
  36. package/dist/src/learning/engine.js +356 -0
  37. package/dist/src/learning/index.js +9 -0
  38. package/dist/src/learning/locator-policy.js +120 -0
  39. package/dist/src/learning/pattern-policy.js +89 -0
  40. package/dist/src/learning/recovery-policy.js +116 -0
  41. package/dist/src/learning/sensor-policy.js +115 -0
  42. package/dist/src/learning/timing-model.js +204 -0
  43. package/dist/src/learning/topology-policy.js +90 -0
  44. package/dist/src/learning/types.js +9 -0
  45. package/dist/src/logging/timeline-logger.js +48 -0
  46. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  47. package/dist/src/mcp/server.js +363 -0
  48. package/dist/src/mcp-entry.js +60 -0
  49. package/dist/src/memory/playbook-seeds.js +200 -0
  50. package/dist/src/memory/recall.js +222 -0
  51. package/dist/src/memory/research.js +104 -0
  52. package/dist/src/memory/seeds.js +101 -0
  53. package/dist/src/memory/service.js +446 -0
  54. package/dist/src/memory/session.js +169 -0
  55. package/dist/src/memory/store.js +451 -0
  56. package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
  57. package/dist/src/monitor/codex-monitor.js +382 -0
  58. package/dist/src/monitor/task-queue.js +97 -0
  59. package/dist/src/monitor/types.js +62 -0
  60. package/dist/src/native/bridge-client.js +412 -0
  61. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  62. package/dist/src/observer/state.js +199 -0
  63. package/dist/src/observer/types.js +43 -0
  64. package/dist/src/orchestrator/state.js +68 -0
  65. package/dist/src/orchestrator/types.js +22 -0
  66. package/dist/src/perception/ax-source.js +162 -0
  67. package/dist/src/perception/cdp-source.js +162 -0
  68. package/dist/src/perception/coordinator.js +771 -0
  69. package/dist/src/perception/frame-differ.js +287 -0
  70. package/dist/src/perception/index.js +22 -0
  71. package/dist/src/perception/manager.js +199 -0
  72. package/dist/src/perception/types.js +47 -0
  73. package/dist/src/perception/vision-source.js +399 -0
  74. package/dist/src/planner/deterministic.js +298 -0
  75. package/dist/src/planner/executor.js +870 -0
  76. package/dist/src/planner/goal-store.js +92 -0
  77. package/dist/src/planner/index.js +21 -0
  78. package/dist/src/planner/planner.js +520 -0
  79. package/dist/src/planner/tool-registry.js +71 -0
  80. package/dist/src/planner/types.js +22 -0
  81. package/dist/src/platform/explorer.js +213 -0
  82. package/dist/src/platform/help-center-markdown.js +527 -0
  83. package/dist/src/platform/learner.js +257 -0
  84. package/dist/src/playbook/engine.js +486 -0
  85. package/dist/src/playbook/index.js +20 -0
  86. package/dist/src/playbook/mcp-recorder.js +204 -0
  87. package/dist/src/playbook/recorder.js +536 -0
  88. package/dist/src/playbook/runner.js +408 -0
  89. package/dist/src/playbook/store.js +312 -0
  90. package/dist/src/playbook/types.js +17 -0
  91. package/dist/src/recovery/detectors.js +156 -0
  92. package/dist/src/recovery/engine.js +327 -0
  93. package/dist/src/recovery/index.js +20 -0
  94. package/dist/src/recovery/strategies.js +274 -0
  95. package/dist/src/recovery/types.js +20 -0
  96. package/dist/src/runtime/accessibility-adapter.js +430 -0
  97. package/dist/src/runtime/app-adapter.js +64 -0
  98. package/dist/src/runtime/applescript-adapter.js +305 -0
  99. package/dist/src/runtime/ax-role-map.js +96 -0
  100. package/dist/src/runtime/browser-adapter.js +52 -0
  101. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  102. package/dist/src/runtime/composite-adapter.js +221 -0
  103. package/dist/src/runtime/execution-contract.js +159 -0
  104. package/dist/src/runtime/executor.js +286 -0
  105. package/dist/src/runtime/locator-cache.js +50 -0
  106. package/dist/src/runtime/planning-loop.js +63 -0
  107. package/dist/src/runtime/service.js +432 -0
  108. package/dist/src/runtime/session-manager.js +63 -0
  109. package/dist/src/runtime/state-observer.js +121 -0
  110. package/dist/src/runtime/vision-adapter.js +225 -0
  111. package/dist/src/state/app-map-types.js +72 -0
  112. package/dist/src/state/app-map.js +1974 -0
  113. package/dist/src/state/entity-tracker.js +108 -0
  114. package/dist/src/state/fusion.js +96 -0
  115. package/dist/src/state/index.js +21 -0
  116. package/dist/src/state/ladder-generator.js +236 -0
  117. package/dist/src/state/persistence.js +156 -0
  118. package/dist/src/state/types.js +17 -0
  119. package/dist/src/state/world-model.js +1456 -0
  120. package/dist/src/supervisor/locks.js +186 -0
  121. package/dist/src/supervisor/supervisor.js +403 -0
  122. package/dist/src/supervisor/types.js +30 -0
  123. package/dist/src/test-mcp-protocol.js +154 -0
  124. package/dist/src/types.js +17 -0
  125. package/dist/src/util/atomic-write.js +133 -0
  126. package/dist/src/util/sanitize.js +146 -0
  127. package/dist-app-maps/com.figma.Desktop.json +959 -0
  128. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  129. package/dist-app-maps/notion.id.json +2831 -0
  130. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  131. package/dist-playbooks/codex-desktop.json +76 -0
  132. package/dist-playbooks/competitor-research-stack.json +122 -0
  133. package/dist-playbooks/davinci-color-grade.json +153 -0
  134. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  135. package/dist-playbooks/davinci-render.json +114 -0
  136. package/dist-playbooks/devto.json +52 -0
  137. package/dist-playbooks/discord.json +41 -0
  138. package/dist-playbooks/google-flow-create-project.json +59 -0
  139. package/dist-playbooks/google-flow-edit-image.json +90 -0
  140. package/dist-playbooks/google-flow-edit-video.json +90 -0
  141. package/dist-playbooks/google-flow-generate-image.json +68 -0
  142. package/dist-playbooks/google-flow-generate-video.json +191 -0
  143. package/dist-playbooks/google-flow-open-project.json +48 -0
  144. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  145. package/dist-playbooks/google-flow-search-assets.json +64 -0
  146. package/dist-playbooks/instagram.json +57 -0
  147. package/dist-playbooks/linkedin.json +52 -0
  148. package/dist-playbooks/n8n.json +43 -0
  149. package/dist-playbooks/reddit.json +52 -0
  150. package/dist-playbooks/threads.json +59 -0
  151. package/dist-playbooks/x-twitter.json +59 -0
  152. package/dist-playbooks/youtube.json +59 -0
  153. package/dist-references/canva.json +646 -0
  154. package/dist-references/codex-desktop.json +305 -0
  155. package/dist-references/davinci-resolve-keyboard.json +594 -0
  156. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  157. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  158. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  159. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  160. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  161. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  162. package/dist-references/devto.json +317 -0
  163. package/dist-references/discord.json +549 -0
  164. package/dist-references/figma.json +1186 -0
  165. package/dist-references/finder.json +146 -0
  166. package/dist-references/google-ads-transparency.json +95 -0
  167. package/dist-references/google-flow.json +649 -0
  168. package/dist-references/instagram.json +341 -0
  169. package/dist-references/linkedin.json +324 -0
  170. package/dist-references/meta-ad-library.json +86 -0
  171. package/dist-references/n8n.json +387 -0
  172. package/dist-references/notes.json +27 -0
  173. package/dist-references/notion.json +163 -0
  174. package/dist-references/reddit.json +341 -0
  175. package/dist-references/threads.json +337 -0
  176. package/dist-references/x-twitter.json +403 -0
  177. package/dist-references/youtube.json +373 -0
  178. package/native/macos-bridge/Package.swift +1 -0
  179. package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
  180. package/native/macos-bridge/Sources/AppManagement.swift +212 -2
  181. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
  182. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  183. package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
  184. package/native/macos-bridge/Sources/main.swift +169 -16
  185. package/native/windows-bridge/Program.cs +5 -0
  186. package/native/windows-bridge/ScreenCapture.cs +124 -0
  187. package/package.json +29 -4
  188. package/scripts/postinstall.cjs +127 -0
  189. package/.claude/commands/automate.md +0 -28
  190. package/.claude/commands/debug-ui.md +0 -19
  191. package/.claude/commands/screenshot.md +0 -15
  192. package/.github/FUNDING.yml +0 -1
  193. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  194. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  195. package/.mcp.json +0 -8
  196. package/DESKTOP_MCP_GUIDE.md +0 -92
  197. package/SECURITY.md +0 -44
  198. package/docs/architecture.md +0 -47
  199. package/install-skills.sh +0 -19
  200. package/mcp-bridge.ts +0 -271
  201. package/mcp-desktop.ts +0 -1221
  202. package/playbooks/instagram.json +0 -41
  203. package/playbooks/instagram_v2.json +0 -201
  204. package/playbooks/x_v1.json +0 -211
  205. package/scripts/devpost-live-loop.mjs +0 -421
  206. package/src/logging/timeline-logger.ts +0 -55
  207. package/src/mcp/server.ts +0 -449
  208. package/src/memory/recall.ts +0 -191
  209. package/src/memory/research.ts +0 -146
  210. package/src/memory/seeds.ts +0 -123
  211. package/src/memory/session.ts +0 -201
  212. package/src/memory/store.ts +0 -434
  213. package/src/memory/types.ts +0 -69
  214. package/src/native/bridge-client.ts +0 -239
  215. package/src/runtime/accessibility-adapter.ts +0 -487
  216. package/src/runtime/app-adapter.ts +0 -169
  217. package/src/runtime/applescript-adapter.ts +0 -376
  218. package/src/runtime/ax-role-map.ts +0 -102
  219. package/src/runtime/browser-adapter.ts +0 -129
  220. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  221. package/src/runtime/composite-adapter.ts +0 -274
  222. package/src/runtime/executor.ts +0 -396
  223. package/src/runtime/planning-loop.ts +0 -81
  224. package/src/runtime/service.ts +0 -448
  225. package/src/runtime/session-manager.ts +0 -50
  226. package/src/runtime/state-observer.ts +0 -136
  227. package/src/runtime/vision-adapter.ts +0 -297
  228. package/src/types.ts +0 -297
  229. package/tests/bridge-client.test.ts +0 -176
  230. package/tests/browser-stealth.test.ts +0 -210
  231. package/tests/composite-adapter.test.ts +0 -64
  232. package/tests/mcp-server.test.ts +0 -151
  233. package/tests/memory-recall.test.ts +0 -339
  234. package/tests/memory-research.test.ts +0 -159
  235. package/tests/memory-seeds.test.ts +0 -120
  236. package/tests/memory-store.test.ts +0 -392
  237. package/tests/types.test.ts +0 -92
  238. package/tsconfig.check.json +0 -17
  239. package/tsconfig.json +0 -19
  240. package/vitest.config.ts +0 -8
  241. /package/{playbooks → dist-references}/devpost.json +0 -0
@@ -0,0 +1,305 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ //
4
+ // This file is part of ScreenHand.
5
+ //
6
+ // ScreenHand is free software: you can redistribute it and/or modify
7
+ // it under the terms of the GNU Affero General Public License as
8
+ // published by the Free Software Foundation, version 3.
9
+ //
10
+ // ScreenHand is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
17
+ import { VALID_TRANSITIONS, JOB_STATES } from "./types.js";
18
+ import { JobStore } from "./store.js";
19
+ export class JobManager {
20
+ store;
21
+ memory;
22
+ supervisor;
23
+ constructor(opts) {
24
+ this.store = new JobStore(opts.jobDir);
25
+ this.memory = opts.memory ?? null;
26
+ this.supervisor = opts.supervisor ?? null;
27
+ }
28
+ init() {
29
+ this.store.init();
30
+ }
31
+ // ── Create ──────────────────────────────────────
32
+ create(opts) {
33
+ const now = new Date().toISOString();
34
+ const id = "job_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 8);
35
+ const steps = (opts.steps ?? []).map((s, i) => {
36
+ const step = { index: i, action: s.action, status: "pending" };
37
+ if (s.target !== undefined)
38
+ step.target = s.target;
39
+ if (s.description !== undefined)
40
+ step.description = s.description;
41
+ if (s.text !== undefined)
42
+ step.text = s.text;
43
+ if (s.keys !== undefined)
44
+ step.keys = s.keys;
45
+ if (s.value !== undefined)
46
+ step.value = s.value;
47
+ return step;
48
+ });
49
+ const job = {
50
+ id,
51
+ task: opts.task,
52
+ state: "queued",
53
+ playbookId: opts.playbookId ?? null,
54
+ sessionId: opts.sessionId ?? null,
55
+ bundleId: opts.bundleId ?? null,
56
+ windowId: opts.windowId ?? null,
57
+ lastStep: -1,
58
+ steps,
59
+ blockReason: null,
60
+ retries: 0,
61
+ maxRetries: opts.maxRetries ?? 3,
62
+ lastError: null,
63
+ tags: opts.tags ?? [],
64
+ priority: opts.priority ?? 10,
65
+ createdAt: now,
66
+ updatedAt: now,
67
+ startedAt: null,
68
+ completedAt: null,
69
+ };
70
+ if (opts.chainId)
71
+ job.chainId = opts.chainId;
72
+ if (opts.dependsOn) {
73
+ // Validate dependency exists to prevent permanently stuck jobs
74
+ const dep = this.store.get(opts.dependsOn);
75
+ if (!dep) {
76
+ throw new Error(`Dependency job ${opts.dependsOn} does not exist`);
77
+ }
78
+ job.dependsOn = opts.dependsOn;
79
+ }
80
+ if (opts.vars)
81
+ job.vars = opts.vars;
82
+ this.store.add(job);
83
+ return job;
84
+ }
85
+ /**
86
+ * Create a chain of linked jobs. Returns all created jobs.
87
+ * Each job depends on the previous one. Variables from prior job outputs
88
+ * are automatically passed forward using {jobId.outputKey} syntax.
89
+ */
90
+ createChain(opts) {
91
+ const chainId = opts.chainId ?? "chain_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 8);
92
+ const created = [];
93
+ let prevId;
94
+ for (const jobOpts of opts.jobs) {
95
+ const job = this.create({
96
+ ...jobOpts,
97
+ chainId,
98
+ ...(prevId ? { dependsOn: prevId } : {}),
99
+ });
100
+ created.push(job);
101
+ prevId = job.id;
102
+ }
103
+ return created;
104
+ }
105
+ // ── State transitions ───────────────────────────
106
+ transition(id, to, opts) {
107
+ const job = this.store.get(id);
108
+ if (!job)
109
+ return { error: `Job ${id} not found` };
110
+ const allowed = VALID_TRANSITIONS[job.state];
111
+ if (!allowed.includes(to)) {
112
+ return { error: `Cannot transition from "${job.state}" to "${to}". Allowed: [${allowed.join(", ")}]` };
113
+ }
114
+ const patch = { state: to };
115
+ if (to === "running") {
116
+ if (!job.startedAt)
117
+ patch.startedAt = new Date().toISOString();
118
+ patch.blockReason = null;
119
+ if (opts?.sessionId)
120
+ patch.sessionId = opts.sessionId;
121
+ }
122
+ if (to === "blocked" || to === "waiting_human") {
123
+ patch.blockReason = opts?.blockReason ?? null;
124
+ }
125
+ if (to === "failed") {
126
+ patch.lastError = opts?.error ?? job.lastError;
127
+ this.recordOutcomeToMemory(job, false);
128
+ }
129
+ if (to === "done") {
130
+ patch.completedAt = new Date().toISOString();
131
+ this.recordOutcomeToMemory(job, true);
132
+ }
133
+ // Re-queue bumps retry count
134
+ if (job.state === "failed" && to === "queued") {
135
+ patch.retries = job.retries + 1;
136
+ if (patch.retries > job.maxRetries) {
137
+ return { error: `Job ${id} has exceeded max retries (${job.maxRetries})` };
138
+ }
139
+ patch.lastError = null;
140
+ patch.blockReason = null;
141
+ }
142
+ const updated = this.store.update(id, patch);
143
+ return updated ?? { error: `Failed to update job ${id}` };
144
+ }
145
+ // ── Step tracking ───────────────────────────────
146
+ /** Mark a step as completed and advance lastStep. Optionally capture output. */
147
+ completeStep(jobId, stepIndex, opts) {
148
+ const job = this.store.get(jobId);
149
+ if (!job)
150
+ return { error: `Job ${jobId} not found` };
151
+ if (job.state !== "running")
152
+ return { error: `Job is not running (state=${job.state})` };
153
+ const step = job.steps[stepIndex];
154
+ if (!step)
155
+ return { error: `Step ${stepIndex} does not exist (total: ${job.steps.length})` };
156
+ step.status = "done";
157
+ step.completedAt = new Date().toISOString();
158
+ if (opts?.durationMs !== undefined)
159
+ step.durationMs = opts.durationMs;
160
+ if (opts?.output !== undefined)
161
+ step.output = opts.output;
162
+ // Also store in job-level outputs for cross-job variable passing
163
+ const patch = { lastStep: Math.max(job.lastStep, stepIndex), steps: job.steps };
164
+ if (opts?.output !== undefined) {
165
+ const outputs = job.outputs ?? {};
166
+ outputs[String(stepIndex)] = opts.output;
167
+ // Also store by step description if available (friendlier key)
168
+ // Include step index to prevent collisions from similar descriptions
169
+ if (step.description) {
170
+ const key = step.description.replace(/[^a-zA-Z0-9_]/g, "_").substring(0, 50);
171
+ outputs[`${key}_${stepIndex}`] = opts.output;
172
+ }
173
+ patch.outputs = outputs;
174
+ }
175
+ return this.store.update(jobId, patch) ?? { error: "Update failed" };
176
+ }
177
+ /** Mark a step as failed. Does NOT transition the job — caller decides (retry vs block vs fail). */
178
+ failStep(jobId, stepIndex, error) {
179
+ const job = this.store.get(jobId);
180
+ if (!job)
181
+ return { error: `Job ${jobId} not found` };
182
+ if (job.state !== "running")
183
+ return { error: `Job is not running (state=${job.state})` };
184
+ const step = job.steps[stepIndex];
185
+ if (!step)
186
+ return { error: `Step ${stepIndex} does not exist` };
187
+ step.status = "failed";
188
+ step.error = error;
189
+ return this.store.update(jobId, { steps: job.steps, lastError: error }) ?? { error: "Update failed" };
190
+ }
191
+ /** Skip a step (e.g., optional step or already done). */
192
+ skipStep(jobId, stepIndex) {
193
+ const job = this.store.get(jobId);
194
+ if (!job)
195
+ return { error: `Job ${jobId} not found` };
196
+ if (job.state !== "running")
197
+ return { error: `Job is not running (state=${job.state})` };
198
+ const step = job.steps[stepIndex];
199
+ if (!step)
200
+ return { error: `Step ${stepIndex} does not exist` };
201
+ step.status = "skipped";
202
+ return this.store.update(jobId, { steps: job.steps }) ?? { error: "Update failed" };
203
+ }
204
+ // ── Resume ──────────────────────────────────────
205
+ /** Get the resume point: next pending step after lastStep. */
206
+ getResumePoint(jobId) {
207
+ const job = this.store.get(jobId);
208
+ if (!job)
209
+ return null;
210
+ for (let i = job.lastStep + 1; i < job.steps.length; i++) {
211
+ if (job.steps[i].status === "pending") {
212
+ return { stepIndex: i, step: job.steps[i] };
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ // ── Queries ─────────────────────────────────────
218
+ get(id) {
219
+ return this.store.get(id);
220
+ }
221
+ list(state) {
222
+ return this.store.list(state);
223
+ }
224
+ /** Pop the next queued job and transition it to running. Skips jobs whose dependency isn't done yet. */
225
+ dequeue(sessionId) {
226
+ const queued = this.store.list("queued")
227
+ .sort((a, b) => a.priority - b.priority || new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
228
+ for (const candidate of queued) {
229
+ // Check dependency — skip if dependent job isn't done
230
+ if (candidate.dependsOn) {
231
+ const dep = this.store.get(candidate.dependsOn);
232
+ if (!dep || dep.state !== "done")
233
+ continue;
234
+ // Resolve variables from dependency outputs
235
+ if (dep.outputs && candidate.vars) {
236
+ for (const [key, val] of Object.entries(candidate.vars)) {
237
+ // {prev.outputKey} → look up from dependency's outputs
238
+ const match = val.match(/^\{prev\.(.+)\}$/);
239
+ if (match?.[1] && dep.outputs[match[1]]) {
240
+ candidate.vars[key] = dep.outputs[match[1]];
241
+ }
242
+ }
243
+ this.store.update(candidate.id, { vars: candidate.vars });
244
+ }
245
+ }
246
+ const opts = {};
247
+ if (sessionId !== undefined)
248
+ opts.sessionId = sessionId;
249
+ const result = this.transition(candidate.id, "running", opts);
250
+ if (!("error" in result))
251
+ return result;
252
+ }
253
+ return null;
254
+ }
255
+ summary() {
256
+ const all = this.store.list();
257
+ const byState = Object.fromEntries(JOB_STATES.map((s) => [s, 0]));
258
+ for (const j of all)
259
+ byState[j.state]++;
260
+ const queued = all
261
+ .filter((j) => j.state === "queued")
262
+ .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
263
+ return {
264
+ total: all.length,
265
+ byState,
266
+ oldestQueued: queued[0]?.createdAt ?? null,
267
+ runningJobIds: all.filter((j) => j.state === "running").map((j) => j.id),
268
+ };
269
+ }
270
+ /** Remove a job entirely. */
271
+ remove(id) {
272
+ return this.store.remove(id);
273
+ }
274
+ /** Prune old terminal jobs. */
275
+ prune() {
276
+ return this.store.prune();
277
+ }
278
+ // ── Private ─────────────────────────────────────
279
+ recordOutcomeToMemory(job, success) {
280
+ if (!this.memory)
281
+ return;
282
+ try {
283
+ const completedSteps = job.steps.filter((s) => s.status === "done");
284
+ if (completedSteps.length === 0)
285
+ return;
286
+ this.memory.appendStrategy({
287
+ id: "strat_" + job.id,
288
+ task: job.task,
289
+ steps: completedSteps.map((s) => ({
290
+ tool: s.action,
291
+ params: s.target ? { target: s.target } : {},
292
+ })),
293
+ totalDurationMs: completedSteps.reduce((sum, s) => sum + (s.durationMs ?? 0), 0),
294
+ successCount: success ? 1 : 0,
295
+ failCount: success ? 0 : 1,
296
+ lastUsed: new Date().toISOString(),
297
+ tags: job.tags,
298
+ fingerprint: completedSteps.map((s) => s.action).join("→"),
299
+ });
300
+ }
301
+ catch {
302
+ // Non-critical — don't let memory failures break job flow
303
+ }
304
+ }
305
+ }