@bybrawe/opencode-loop 0.5.37 → 0.6.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 (81) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +31 -1
  3. package/examples/OPENCODE2-0.6.0.md +54 -0
  4. package/package.json +12 -8
  5. package/scripts/branch-hygiene.mjs +326 -0
  6. package/scripts/comprehensive-test.mjs +19 -10
  7. package/scripts/control-plane-until-test.mjs +39 -0
  8. package/scripts/fixtures/opencode2-real-adapter-probe.js +6 -1
  9. package/scripts/goal-compaction-safety-test.mjs +68 -0
  10. package/scripts/goal-companion-test.mjs +1 -1
  11. package/scripts/host-adapter-contract-test.mjs +1 -0
  12. package/scripts/install-node.mjs +105 -58
  13. package/scripts/install-test.mjs +6 -1
  14. package/scripts/install-with-goals.mjs +17 -4
  15. package/scripts/job-workspace-test.mjs +11 -1
  16. package/scripts/loopd-test.mjs +74 -4
  17. package/scripts/loopd.mjs +153 -25
  18. package/scripts/native-cli-host-canary.mjs +118 -0
  19. package/scripts/native-cli-io-test.mjs +66 -0
  20. package/scripts/native-cli-test.mjs +130 -0
  21. package/scripts/native-coexistence-test.mjs +38 -0
  22. package/scripts/native-entry-test.mjs +77 -0
  23. package/scripts/native-guide-test.mjs +45 -0
  24. package/scripts/native-install-pair-test.mjs +73 -0
  25. package/scripts/native-install-test.mjs +107 -0
  26. package/scripts/native-lifecycle-test.mjs +100 -0
  27. package/scripts/native-plugin-lifetime-test.mjs +92 -0
  28. package/scripts/native-presets-test.mjs +227 -0
  29. package/scripts/native-process-tree-fixture.mjs +72 -0
  30. package/scripts/native-process-tree-test.mjs +40 -0
  31. package/scripts/native-release.mjs +121 -0
  32. package/scripts/native-session-location-test.mjs +86 -0
  33. package/scripts/native-shell-test.mjs +38 -0
  34. package/scripts/native-v2-test.mjs +262 -0
  35. package/scripts/network-recovery-test.mjs +38 -1
  36. package/scripts/opencode-host.mjs +47 -0
  37. package/scripts/opencode2-loop-canary.mjs +119 -10
  38. package/scripts/package-native-smoke.mjs +31 -0
  39. package/scripts/publish-workflow-test.mjs +60 -14
  40. package/scripts/pulse-check.mjs +246 -0
  41. package/scripts/run-finalization-test.mjs +33 -0
  42. package/scripts/server-entry-test.mjs +15 -0
  43. package/scripts/session-error-recovery-test.mjs +186 -0
  44. package/scripts/session-status-test.mjs +42 -0
  45. package/scripts/v2-command-adapter-test.mjs +14 -2
  46. package/scripts/v2-plugin-sentinel-test.mjs +35 -4
  47. package/scripts/v2-runtime-adapter-test.mjs +75 -0
  48. package/src/index.js +227 -53
  49. package/src/native.js +3135 -0
  50. package/src/server.js +4055 -0
  51. package/src/source/core/continuation.js +23 -0
  52. package/src/source/core/jobs.js +17 -0
  53. package/src/source/core/process.js +6 -4
  54. package/src/source/core/progress.js +58 -0
  55. package/src/source/legacy-v1.js +14 -3
  56. package/src/source/native.js +1 -0
  57. package/src/source/opencode/host.js +1 -0
  58. package/src/source/opencode/loop-commands.js +1 -27
  59. package/src/source/opencode/loop-registration.js +2 -12
  60. package/src/source/opencode/messages.js +1 -1
  61. package/src/source/opencode2/capabilities.js +1 -0
  62. package/src/source/opencode2/commands.js +52 -11
  63. package/src/source/opencode2/event-bridge.js +15 -3
  64. package/src/source/opencode2/events.js +31 -0
  65. package/src/source/opencode2/experimental.js +53 -7
  66. package/src/source/opencode2/native-companion.js +24 -0
  67. package/src/source/opencode2/native-plugin.js +153 -0
  68. package/src/source/opencode2/native-policy.js +106 -0
  69. package/src/source/opencode2/native-runtime.js +464 -0
  70. package/src/source/opencode2/native-shell.js +81 -0
  71. package/src/source/opencode2/prompt-runtime.js +38 -6
  72. package/src/source/opencode2/runtime-adapter.js +15 -2
  73. package/src/source/opencode2/session-location.js +16 -0
  74. package/src/source/runtime/executor.js +83 -0
  75. package/src/source/runtime/goal-steering.js +17 -3
  76. package/src/source/runtime/job-workspace.js +11 -2
  77. package/src/source/runtime/run-finalization.js +10 -0
  78. package/src/source/runtime/session-status.js +16 -2
  79. package/src/source/runtime/terminal-guard.js +32 -7
  80. package/src/source/server.js +26 -0
  81. package/src/v1.js +1 -0
package/src/native.js ADDED
@@ -0,0 +1,3135 @@
1
+ // @bun
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // src/source/core/args.js
15
+ function now() {
16
+ return Date.now();
17
+ }
18
+ function safeID(value) {
19
+ return String(value || "job").replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "job";
20
+ }
21
+ function parseDuration(value) {
22
+ const input = String(value || "").trim();
23
+ if (input === "0")
24
+ return 0;
25
+ const match = input.match(/^(\d+)\s*(ms|s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/i);
26
+ if (!match)
27
+ return null;
28
+ const amount = Number.parseInt(match[1], 10);
29
+ const unit = match[2].toLowerCase();
30
+ if (!Number.isFinite(amount) || amount < 0)
31
+ return null;
32
+ if (unit === "ms")
33
+ return amount;
34
+ if (unit.startsWith("s"))
35
+ return amount * 1000;
36
+ if (unit.startsWith("m"))
37
+ return amount * 60000;
38
+ if (unit.startsWith("h"))
39
+ return amount * 3600000;
40
+ if (unit.startsWith("d"))
41
+ return amount * 86400000;
42
+ return null;
43
+ }
44
+ function durationToText(ms) {
45
+ if (ms === 0)
46
+ return "every idle";
47
+ if (!Number.isFinite(ms))
48
+ return "unknown";
49
+ if (ms % 86400000 === 0)
50
+ return `${ms / 86400000}d`;
51
+ if (ms % 3600000 === 0)
52
+ return `${ms / 3600000}h`;
53
+ if (ms % 60000 === 0)
54
+ return `${ms / 60000}m`;
55
+ if (ms % 1000 === 0)
56
+ return `${ms / 1000}s`;
57
+ return `${ms}ms`;
58
+ }
59
+ function splitFirst(input) {
60
+ const match = String(input || "").trim().match(/^(\S+)\s*([\s\S]*)$/);
61
+ if (!match)
62
+ return ["", ""];
63
+ return [match[1], (match[2] || "").trim()];
64
+ }
65
+ function stripOuterQuotes(value) {
66
+ const input = String(value || "").trim();
67
+ if (input.startsWith('"') && input.endsWith('"') || input.startsWith("'") && input.endsWith("'")) {
68
+ return input.slice(1, -1);
69
+ }
70
+ return input;
71
+ }
72
+ function escapeRegExp(value) {
73
+ return String(value).replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
74
+ }
75
+ function takeFlag(rest, flag) {
76
+ const pattern = new RegExp(`(^|\\s)${escapeRegExp(flag)}(?=\\s|$)`, "i");
77
+ const found = pattern.test(rest);
78
+ return [found, rest.replace(pattern, " ").replace(/\s+/g, " ").trim()];
79
+ }
80
+ function takeFlagValue(rest, flag) {
81
+ const pattern = new RegExp(`(^|\\s)${escapeRegExp(flag)}\\s+(?:"([^"]*)"|'([^']*)'|(\\S+))`, "i");
82
+ const match = rest.match(pattern);
83
+ if (!match)
84
+ return [undefined, rest];
85
+ const value = match[2] ?? match[3] ?? match[4];
86
+ return [value, rest.replace(pattern, " ").replace(/\s+/g, " ").trim()];
87
+ }
88
+ function takeAllFlagValues(rest, flag) {
89
+ const values = [];
90
+ let current = rest;
91
+ while (true) {
92
+ const [value, next] = takeFlagValue(current, flag);
93
+ if (value === undefined)
94
+ return [values, current];
95
+ values.push(value);
96
+ current = next;
97
+ }
98
+ }
99
+ function parsePositiveInt(value, fallback = 0) {
100
+ const parsed = Number.parseInt(String(value || ""), 10);
101
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
102
+ }
103
+ function parseNonNegativeInt(value, fallback = 0) {
104
+ const parsed = Number.parseInt(String(value || ""), 10);
105
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
106
+ }
107
+ function parseCompactEvery(value) {
108
+ const duration = parseDuration(value);
109
+ if (duration !== null)
110
+ return { compactEveryMs: duration };
111
+ const runs = parsePositiveInt(value, 0);
112
+ return runs > 0 ? { compactEveryRuns: runs } : {};
113
+ }
114
+ function parseLoopArgs(raw, defaults = {}) {
115
+ let input = stripOuterQuotes(String(raw || "").trim());
116
+ let first = "";
117
+ let rest = input;
118
+ let intervalMs = defaults.intervalMs ?? null;
119
+ if (!input && defaults.action) {
120
+ rest = defaults.action;
121
+ } else {
122
+ [first, rest] = splitFirst(input);
123
+ if (first === "--watch") {
124
+ intervalMs = defaults.intervalMs ?? 0;
125
+ rest = input;
126
+ } else if (first) {
127
+ const parsedDuration = parseDuration(first);
128
+ if (parsedDuration !== null)
129
+ intervalMs = parsedDuration;
130
+ else if (intervalMs === null)
131
+ return { ok: false, error: "Usage: /loop 0s <prompt> | /loop 5m <prompt> | /loop-goal <objective> | /loop-command 200m /compact | /loop-shell 10m npm test | /loop --watch progress.md <prompt>" };
132
+ else
133
+ rest = input;
134
+ }
135
+ }
136
+ if (intervalMs === null)
137
+ intervalMs = 0;
138
+ const job = {
139
+ id: `${now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`,
140
+ name: defaults.name,
141
+ action: defaults.action || "",
142
+ kind: defaults.kind || undefined,
143
+ intervalMs,
144
+ immediate: defaults.immediate ?? true,
145
+ maxRuns: defaults.maxRuns ?? 0,
146
+ maxRuntimeMs: defaults.maxRuntimeMs ?? 0,
147
+ maxFailures: defaults.maxFailures ?? 0,
148
+ timeoutMs: defaults.timeoutMs ?? 0,
149
+ until: defaults.until,
150
+ stopFile: defaults.stopFile,
151
+ progressFile: defaults.progressFile,
152
+ promptFile: defaults.promptFile,
153
+ includeFiles: Array.isArray(defaults.includeFiles) ? [...defaults.includeFiles] : [],
154
+ watchPaths: Array.isArray(defaults.watchPaths) ? [...defaults.watchPaths] : [],
155
+ compactEveryRuns: defaults.compactEveryRuns ?? 0,
156
+ compactEveryMs: defaults.compactEveryMs ?? 0,
157
+ testCommand: defaults.testCommand,
158
+ verifyCommand: defaults.verifyCommand,
159
+ preflightCommand: defaults.preflightCommand,
160
+ postrunCommand: defaults.postrunCommand,
161
+ notifyCommand: defaults.notifyCommand,
162
+ branch: defaults.branch,
163
+ branchDone: false,
164
+ goalStatus: defaults.goalStatus,
165
+ goalFile: defaults.goalFile,
166
+ goalAcceptance: Array.isArray(defaults.goalAcceptance) ? [...defaults.goalAcceptance] : [],
167
+ goalChecks: Array.isArray(defaults.goalChecks) ? [...defaults.goalChecks] : [],
168
+ goalCompleteWhenChecksPass: defaults.goalCompleteWhenChecksPass ?? false,
169
+ goalRequireEvidence: defaults.goalRequireEvidence,
170
+ goalRequireChecksPass: defaults.goalRequireChecksPass,
171
+ goalEvidenceFile: defaults.goalEvidenceFile,
172
+ goalSummary: defaults.goalSummary || "",
173
+ goalEvidence: defaults.goalEvidence || "",
174
+ goalBlockedReason: defaults.goalBlockedReason || "",
175
+ goalProgress: Array.isArray(defaults.goalProgress) ? [...defaults.goalProgress] : [],
176
+ maxNoProgress: defaults.maxNoProgress,
177
+ noProgressCount: defaults.noProgressCount ?? 0,
178
+ lastProgressAt: defaults.lastProgressAt ?? 0,
179
+ noOverlap: defaults.noOverlap ?? true,
180
+ safe: defaults.safe ?? false,
181
+ quiet: defaults.quiet ?? false,
182
+ askNever: defaults.askNever ?? false,
183
+ pauseOnVerifyFail: defaults.pauseOnVerifyFail ?? false,
184
+ gitCheckpoint: defaults.gitCheckpoint ?? false,
185
+ checkpointOnly: defaults.checkpointOnly ?? false,
186
+ dryRun: defaults.dryRun ?? false,
187
+ multi: defaults.multi ?? false,
188
+ batch: defaults.batch ?? 0,
189
+ runCount: 0,
190
+ failureCount: 0,
191
+ lastRunAt: 0,
192
+ lastCompactAt: 0,
193
+ lastCompactRunCount: 0,
194
+ watchSnapshot: {},
195
+ watchTriggered: false,
196
+ createdAt: new Date().toISOString(),
197
+ enabled: true,
198
+ paused: false
199
+ };
200
+ let found;
201
+ let value;
202
+ [found, rest] = takeFlag(rest, "--no-now");
203
+ if (found)
204
+ job.immediate = false;
205
+ [found, rest] = takeFlag(rest, "--now");
206
+ if (found)
207
+ job.immediate = true;
208
+ [found, rest] = takeFlag(rest, "--no-overlap");
209
+ if (found)
210
+ job.noOverlap = true;
211
+ [found, rest] = takeFlag(rest, "--allow-overlap");
212
+ if (found)
213
+ job.noOverlap = false;
214
+ [found, rest] = takeFlag(rest, "--safe");
215
+ if (found)
216
+ job.safe = true;
217
+ [found, rest] = takeFlag(rest, "--quiet");
218
+ if (found)
219
+ job.quiet = true;
220
+ [found, rest] = takeFlag(rest, "--ask-never");
221
+ if (found)
222
+ job.askNever = true;
223
+ [found, rest] = takeFlag(rest, "--git-checkpoint");
224
+ if (found)
225
+ job.gitCheckpoint = true;
226
+ [found, rest] = takeFlag(rest, "--checkpoint-only");
227
+ if (found)
228
+ job.checkpointOnly = true;
229
+ [found, rest] = takeFlag(rest, "--pause-on-verify-fail");
230
+ if (found)
231
+ job.pauseOnVerifyFail = true;
232
+ [found, rest] = takeFlag(rest, "--dry-run");
233
+ if (found)
234
+ job.dryRun = true;
235
+ [found, rest] = takeFlag(rest, "--multi");
236
+ if (found)
237
+ job.multi = true;
238
+ [found, rest] = takeFlag(rest, "--replace");
239
+ if (found)
240
+ job.multi = false;
241
+ [found, rest] = takeFlag(rest, "--prompt");
242
+ if (found)
243
+ job.kind = "prompt";
244
+ [found, rest] = takeFlag(rest, "--ask");
245
+ if (found)
246
+ job.kind = "prompt";
247
+ [found, rest] = takeFlag(rest, "--command");
248
+ if (found)
249
+ job.kind = "command";
250
+ [found, rest] = takeFlag(rest, "--cmd");
251
+ if (found)
252
+ job.kind = "command";
253
+ [found, rest] = takeFlag(rest, "--slash");
254
+ if (found)
255
+ job.kind = "command";
256
+ [found, rest] = takeFlag(rest, "--shell");
257
+ if (found)
258
+ job.kind = "shell";
259
+ [found, rest] = takeFlag(rest, "--compact");
260
+ if (found)
261
+ job.kind = "compact";
262
+ [found, rest] = takeFlag(rest, "--goal");
263
+ if (found)
264
+ job.kind = "goal";
265
+ [found, rest] = takeFlag(rest, "--complete-when-checks-pass");
266
+ if (found)
267
+ job.goalCompleteWhenChecksPass = true;
268
+ [found, rest] = takeFlag(rest, "--no-complete-when-checks-pass");
269
+ if (found)
270
+ job.goalCompleteWhenChecksPass = false;
271
+ [found, rest] = takeFlag(rest, "--require-evidence");
272
+ if (found)
273
+ job.goalRequireEvidence = true;
274
+ [found, rest] = takeFlag(rest, "--allow-weak-evidence");
275
+ if (found)
276
+ job.goalRequireEvidence = false;
277
+ [found, rest] = takeFlag(rest, "--require-checks-pass");
278
+ if (found)
279
+ job.goalRequireChecksPass = true;
280
+ [found, rest] = takeFlag(rest, "--allow-complete-without-checks");
281
+ if (found)
282
+ job.goalRequireChecksPass = false;
283
+ [found, rest] = takeFlag(rest, "--allow-complete-with-failing-checks");
284
+ if (found)
285
+ job.goalRequireChecksPass = false;
286
+ [value, rest] = takeFlagValue(rest, "--name");
287
+ if (value !== undefined)
288
+ job.name = value.trim();
289
+ [value, rest] = takeFlagValue(rest, "--max-runs");
290
+ if (value !== undefined)
291
+ job.maxRuns = parsePositiveInt(value, 0);
292
+ [value, rest] = takeFlagValue(rest, "--max-turns");
293
+ if (value !== undefined)
294
+ job.maxRuns = parsePositiveInt(value, 0);
295
+ [value, rest] = takeFlagValue(rest, "--max-no-progress");
296
+ if (value !== undefined)
297
+ job.maxNoProgress = parseNonNegativeInt(value, DEFAULT_GOAL_MAX_NO_PROGRESS);
298
+ [value, rest] = takeFlagValue(rest, "--timeout");
299
+ if (value !== undefined)
300
+ job.timeoutMs = parseDuration(value) ?? 0;
301
+ [value, rest] = takeFlagValue(rest, "--max-runtime");
302
+ if (value !== undefined)
303
+ job.maxRuntimeMs = parseDuration(value) ?? 0;
304
+ [value, rest] = takeFlagValue(rest, "--max-failures");
305
+ if (value !== undefined)
306
+ job.maxFailures = parsePositiveInt(value, 0);
307
+ [value, rest] = takeFlagValue(rest, "--until");
308
+ if (value !== undefined)
309
+ job.until = stripOuterQuotes(value);
310
+ [value, rest] = takeFlagValue(rest, "--stop-file");
311
+ if (value !== undefined)
312
+ job.stopFile = stripOuterQuotes(value);
313
+ [value, rest] = takeFlagValue(rest, "--progress-file");
314
+ if (value !== undefined)
315
+ job.progressFile = stripOuterQuotes(value);
316
+ [value, rest] = takeFlagValue(rest, "--prompt-file");
317
+ if (value !== undefined)
318
+ job.promptFile = stripOuterQuotes(value);
319
+ [value, rest] = takeFlagValue(rest, "--goal-file");
320
+ if (value !== undefined)
321
+ job.goalFile = stripOuterQuotes(value);
322
+ [value, rest] = takeFlagValue(rest, "--evidence-file");
323
+ if (value !== undefined)
324
+ job.goalEvidenceFile = stripOuterQuotes(value);
325
+ [value, rest] = takeFlagValue(rest, "--test");
326
+ if (value !== undefined)
327
+ job.testCommand = stripOuterQuotes(value);
328
+ [value, rest] = takeFlagValue(rest, "--verify");
329
+ if (value !== undefined)
330
+ job.verifyCommand = stripOuterQuotes(value);
331
+ [value, rest] = takeFlagValue(rest, "--preflight");
332
+ if (value !== undefined)
333
+ job.preflightCommand = stripOuterQuotes(value);
334
+ [value, rest] = takeFlagValue(rest, "--postrun");
335
+ if (value !== undefined)
336
+ job.postrunCommand = stripOuterQuotes(value);
337
+ [value, rest] = takeFlagValue(rest, "--notify");
338
+ if (value !== undefined)
339
+ job.notifyCommand = stripOuterQuotes(value);
340
+ [value, rest] = takeFlagValue(rest, "--branch");
341
+ if (value !== undefined)
342
+ job.branch = stripOuterQuotes(value);
343
+ [value, rest] = takeFlagValue(rest, "--batch");
344
+ if (value !== undefined)
345
+ job.batch = parsePositiveInt(value, 0);
346
+ [value, rest] = takeFlagValue(rest, "--compact-every");
347
+ if (value !== undefined)
348
+ Object.assign(job, parseCompactEvery(value));
349
+ const watch = takeAllFlagValues(rest, "--watch");
350
+ job.watchPaths.push(...watch[0].map(stripOuterQuotes).filter(Boolean));
351
+ rest = watch[1];
352
+ const includes = takeAllFlagValues(rest, "--include-file");
353
+ job.includeFiles.push(...includes[0].map(stripOuterQuotes).filter(Boolean));
354
+ rest = includes[1];
355
+ const acceptances = takeAllFlagValues(rest, "--acceptance");
356
+ job.goalAcceptance.push(...acceptances[0].map(stripOuterQuotes).filter(Boolean));
357
+ rest = acceptances[1];
358
+ const success = takeAllFlagValues(rest, "--success");
359
+ job.goalAcceptance.push(...success[0].map(stripOuterQuotes).filter(Boolean));
360
+ rest = success[1];
361
+ const checks = takeAllFlagValues(rest, "--check");
362
+ job.goalChecks.push(...checks[0].map(stripOuterQuotes).filter(Boolean));
363
+ rest = checks[1];
364
+ job.action = stripOuterQuotes(rest || job.action || "");
365
+ job.watchPaths = [...new Set(job.watchPaths)];
366
+ job.includeFiles = [...new Set(job.includeFiles)];
367
+ job.goalAcceptance = [...new Set(job.goalAcceptance || [])];
368
+ job.goalChecks = [...new Set(job.goalChecks || [])];
369
+ if (String(job.kind || "").toLowerCase() === "goal") {
370
+ job.name = job.name || "goal";
371
+ job.goalStatus = job.goalStatus || "active";
372
+ job.safe = job.safe !== false;
373
+ job.askNever = job.askNever !== false;
374
+ job.noOverlap = job.noOverlap !== false;
375
+ job.goalRequireEvidence = job.goalRequireEvidence !== false;
376
+ job.goalRequireChecksPass = job.goalRequireChecksPass ?? job.goalChecks.length > 0;
377
+ job.maxNoProgress = job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS;
378
+ }
379
+ job.lastRunAt = job.immediate ? 0 : now();
380
+ if (!job.action && !job.promptFile && !job.goalFile)
381
+ return { ok: false, error: "Missing action. Example: /loop 0s continue from progress.md, /loop-goal ship the feature, or /loop 0s --prompt-file loop-prompt.md" };
382
+ return { ok: true, job };
383
+ }
384
+ var DEFAULT_GOAL_MAX_NO_PROGRESS = 3;
385
+
386
+ // src/source/core/continuation.js
387
+ function isContinuationShorthand(value) {
388
+ return CONTINUATION_SHORTHANDS.has(String(value || "").trim().toLowerCase().replace(/\s+/g, " "));
389
+ }
390
+ function isCompletionBoundedContinuation(value) {
391
+ const text = String(value || "").trim();
392
+ return COMPLETION_BOUNDED_PATTERNS.some((pattern) => pattern.test(text));
393
+ }
394
+ function continuationProjectInstruction(value) {
395
+ if (!isContinuationShorthand(value) && !isCompletionBoundedContinuation(value))
396
+ return "";
397
+ const finish = isCompletionBoundedContinuation(value) ? " If you believe the project is finished, perform a fresh verification pass before declaring completion; report both that the project is complete and that no work remains only when you have concrete current evidence." : "";
398
+ return `Treat this as continuation of the current project and conversation, not a fresh task. Inspect the repository state, relevant files, TODO/progress notes, recent changes, and git status as needed to identify the next unfinished step. Continue from existing work, do not redo completed work, and verify meaningful changes when practical.${finish}`;
399
+ }
400
+ var CONTINUATION_SHORTHANDS, COMPLETION_BOUNDED_PATTERNS;
401
+ var init_continuation = __esm(() => {
402
+ CONTINUATION_SHORTHANDS = new Set([
403
+ "continue",
404
+ "continue.",
405
+ "continue working",
406
+ "keep going",
407
+ "go on",
408
+ "devam",
409
+ "devam et",
410
+ "devam et.",
411
+ "devam et bakal\u0131m"
412
+ ]);
413
+ COMPLETION_BOUNDED_PATTERNS = [
414
+ /\bbitene kadar\b/i,
415
+ /\b(?:tamamen|komple) projeyi bitir\b/i,
416
+ /\bi\u015Fi bitir\b/i,
417
+ /\buntil (?:it(?:'s| is) )?(?:done|complete|completed|finished)\b/i,
418
+ /\bfinish (?:the )?(?:project|task|work)\b/i,
419
+ /\bkeep going until\b/i
420
+ ];
421
+ });
422
+
423
+ // src/source/core/jobs.js
424
+ function presetDefaults(name) {
425
+ if (name === "loop-compact")
426
+ return { intervalMs: parseDuration("200m"), action: "/compact", kind: "compact", name: "compact", immediate: false };
427
+ if (name === "loop-command" || name === "loop-cmd")
428
+ return { intervalMs: 0, kind: "command", name: "command", immediate: false };
429
+ if (name === "loop-prompt")
430
+ return { intervalMs: 0, kind: "prompt", name: "prompt", immediate: true };
431
+ if (name === "loop-ask")
432
+ return { intervalMs: 0, kind: "prompt", name: "ask", immediate: false };
433
+ if (name === "loop-shell")
434
+ return { intervalMs: 0, kind: "shell", name: "shell", immediate: false };
435
+ if (name === "loop-testfix")
436
+ return { intervalMs: 0, name: "testfix", safe: true, askNever: true, verifyCommand: "npm test", testfixPreset: true, action: "Run the project tests. Fix failures. Re-run the tests. Test command hint: npm test" };
437
+ if (name === "loop-progress")
438
+ return { intervalMs: 0, name: "progress", safe: true, askNever: true, progressFile: "progress.md", action: "Read progress.md and continue the next unfinished TODO. Mark completed TODOs with [x]. Add useful TODOs when you discover them." };
439
+ if (name === "loop-safe-dev")
440
+ return { intervalMs: 0, name: "safe-dev", safe: true, askNever: true, noOverlap: true, checkpointOnly: true, batch: 5, progressFile: "progress.md", action: "Develop the project from progress.md. Work in small safe batches. Mark completed TODOs with [x]. Add new ideas to progress.md. Run tests/lint/build if available." };
441
+ return { intervalMs: 0, name: "dev", askNever: true, progressFile: "progress.md", action: "Continue developing the project from progress.md. Mark completed TODOs with [x]. Add new ideas to progress.md. Run tests/lint/build if available." };
442
+ }
443
+ function jobLabel(job) {
444
+ const title = job.name ? `${job.name}: ` : "";
445
+ const kind = job.kind ? ` [${job.kind}]` : "";
446
+ const limit = job.maxRuns > 0 ? `, max ${job.maxRuns}` : "";
447
+ const runtime = job.maxRuntimeMs > 0 ? `, runtime ${durationToText(job.maxRuntimeMs)}` : "";
448
+ const timeout = job.timeoutMs > 0 ? `, timeout ${durationToText(job.timeoutMs)}` : "";
449
+ const compact = job.compactEveryRuns > 0 ? `, compact every ${job.compactEveryRuns} runs` : job.compactEveryMs > 0 ? `, compact every ${durationToText(job.compactEveryMs)}` : "";
450
+ const verify = job.verifyCommand ? ", verify" : "";
451
+ const preflight = job.preflightCommand ? ", preflight" : "";
452
+ const failures = job.maxFailures > 0 ? `, max failures ${job.maxFailures}` : "";
453
+ const noProgress = isGoalJob(job) && (job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS) > 0 ? `, max no-progress ${job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS}` : "";
454
+ const stopFile = job.stopFile ? ", stop-file" : "";
455
+ const watch = job.watchPaths?.length ? `, watch ${job.watchPaths.join(",")}` : "";
456
+ const paused = job.paused ? ", paused" : "";
457
+ return `${title}${durationToText(job.intervalMs)}${kind} -> ${job.action || `[prompt-file: ${job.promptFile}]`}${limit}${runtime}${timeout}${compact}${verify}${preflight}${failures}${noProgress}${stopFile}${watch}${paused}`;
458
+ }
459
+ function matchJob(job, target, index) {
460
+ const text = String(target || "").trim();
461
+ if (!text || text.toLowerCase() === "all")
462
+ return true;
463
+ return job.id === text || job.name === text || String(index + 1) === text;
464
+ }
465
+ function actionKind(action, job = {}) {
466
+ const text = String(action || "").trim();
467
+ const forced = String(job.kind || "").trim().toLowerCase();
468
+ if (forced === "compact")
469
+ return "compact";
470
+ if (forced === "goal")
471
+ return "goal";
472
+ if (text === "/compact" || text === "/summarize")
473
+ return "compact";
474
+ if (forced === "prompt" || forced === "ask")
475
+ return "prompt";
476
+ if (forced === "command" || forced === "cmd" || forced === "slash")
477
+ return "command";
478
+ if (forced === "shell")
479
+ return "shell";
480
+ if (text.startsWith("/"))
481
+ return "command";
482
+ if (text.startsWith("!") || text.startsWith("$"))
483
+ return "shell";
484
+ return "prompt";
485
+ }
486
+ function decoratePrompt(job) {
487
+ const additions = [];
488
+ const continuation = continuationProjectInstruction(job.action);
489
+ if (continuation)
490
+ additions.push(continuation);
491
+ if (job.progressFile)
492
+ additions.push(`Use ${job.progressFile} as the main progress/TODO state file. Read it before choosing the next task and update it after work.`);
493
+ if (job.lastVerifyFailure)
494
+ additions.push("Previous verify command failed. Fix this before moving on. Failure summary: " + String(job.lastVerifyFailure).slice(0, 1200));
495
+ if (job.askNever)
496
+ additions.push("Do not ask the user questions. Make reasonable assumptions and continue. Only write a short BLOCKED note if truly blocked.");
497
+ if (job.safe)
498
+ additions.push("Safety rules: do not run destructive commands such as git reset, git clean, rm -rf, del /s, rmdir /s, force push, production deploys, production migrations, terraform destroy, or deleting user data. If such an action seems needed, write a BLOCKED note instead.");
499
+ if (job.batch > 0)
500
+ additions.push(`Batch rule: in this run, work on at most ${job.batch} unfinished TODO item(s). Mark completed items with [x].`);
501
+ if (job.quiet)
502
+ additions.push("Keep replies short. Summarize only what changed, tests run, and next step.");
503
+ if (job.testCommand)
504
+ additions.push(`After making changes, run this test/check command if applicable: ${job.testCommand}. If it fails, fix the failure and try again.`);
505
+ if (job.checkpointOnly || job.gitCheckpoint)
506
+ additions.push("Keep changes incremental and easy to review because the loop will create a checkpoint after the run.");
507
+ if (!additions.length)
508
+ return job.action;
509
+ return `${job.action}
510
+
511
+ OpenCode loop instructions:
512
+ - ${additions.join(`
513
+ - `)}`;
514
+ }
515
+ function isGoalJob(job) {
516
+ return String(job?.kind || "").toLowerCase() === "goal";
517
+ }
518
+ function goalStatusText(job) {
519
+ const status = job?.goalStatus || (isGoalJob(job) ? "active" : "");
520
+ if (!status)
521
+ return "";
522
+ if (status === "completed")
523
+ return "completed";
524
+ if (status === "blocked")
525
+ return "blocked";
526
+ if (job?.paused)
527
+ return "paused";
528
+ return status;
529
+ }
530
+ function applyTestfixPreset(job, defaults = {}) {
531
+ if (defaults.testfixPreset) {
532
+ const defaultCommand = String(defaults.verifyCommand || "npm test");
533
+ const parsedAction = String(job.action || "").trim();
534
+ const usedDefaultAction = parsedAction === String(defaults.action || "").trim();
535
+ if (!usedDefaultAction && job.verifyCommand === defaults.verifyCommand) {
536
+ job.verifyCommand = parsedAction;
537
+ job.action = `Run the project tests. Fix failures. Re-run the tests. Test command hint: ${parsedAction}`;
538
+ } else if (usedDefaultAction && job.verifyCommand !== defaults.verifyCommand) {
539
+ job.action = `Run the project tests. Fix failures. Re-run the tests. Test command hint: ${job.verifyCommand || defaultCommand}`;
540
+ }
541
+ }
542
+ return job;
543
+ }
544
+ var init_jobs = __esm(() => {
545
+ init_continuation();
546
+ });
547
+
548
+ // src/source/core/state.js
549
+ import { promises as fs } from "fs";
550
+ import os from "os";
551
+ import path3 from "path";
552
+ function stateDir(directory) {
553
+ return path3.join(directory, STATE_DIR);
554
+ }
555
+ function statePath(directory, sessionID) {
556
+ return path3.join(stateDir(directory), `${safeID(sessionID)}.json`);
557
+ }
558
+ async function ensureDir(directory) {
559
+ await fs.mkdir(directory, { recursive: true });
560
+ }
561
+ function stateLockKey(directory, sessionID) {
562
+ return `${path3.resolve(directory)}:${safeID(sessionID)}`;
563
+ }
564
+ async function withStateWriteLock(directory, sessionID, fn) {
565
+ const key = stateLockKey(directory, sessionID);
566
+ const previous = stateWriteLocks.get(key) || Promise.resolve();
567
+ let release;
568
+ const current = new Promise((resolve) => {
569
+ release = resolve;
570
+ });
571
+ const next = previous.catch(() => {}).then(() => current);
572
+ stateWriteLocks.set(key, next);
573
+ await previous.catch(() => {});
574
+ try {
575
+ return await fn();
576
+ } finally {
577
+ release();
578
+ if (stateWriteLocks.get(key) === next)
579
+ stateWriteLocks.delete(key);
580
+ }
581
+ }
582
+ async function readStateFile(directory, sessionID) {
583
+ const target = statePath(directory, sessionID);
584
+ const attempts = 5;
585
+ for (let attempt = 0;attempt < attempts; attempt++) {
586
+ try {
587
+ const parsed = JSON.parse(await fs.readFile(target, "utf8"));
588
+ return { version: 4, jobs: Array.isArray(parsed.jobs) ? parsed.jobs : [] };
589
+ } catch (error) {
590
+ if (error?.code === "ENOENT")
591
+ return { version: 4, jobs: [] };
592
+ const transient = error instanceof SyntaxError || isRetriableStateWriteError(error);
593
+ if (!transient || attempt === attempts - 1)
594
+ break;
595
+ await delay(25 * (attempt + 1));
596
+ }
597
+ }
598
+ try {
599
+ await ensureDir(stateDir(directory));
600
+ await fs.copyFile(target, `${target}.corrupt-${Date.now()}`);
601
+ } catch {}
602
+ return { version: 4, jobs: [] };
603
+ }
604
+ async function readState(directory, sessionID) {
605
+ const state = await readStateFile(directory, sessionID);
606
+ Object.defineProperty(state, STATE_BASELINE, {
607
+ value: structuredClone(state.jobs || []),
608
+ enumerable: false,
609
+ configurable: false,
610
+ writable: true
611
+ });
612
+ return state;
613
+ }
614
+ function isRetriableStateWriteError(error) {
615
+ const code = error?.code;
616
+ return code === "EPERM" || code === "EACCES" || code === "EBUSY" || code === "EEXIST" || code === "EAGAIN";
617
+ }
618
+ async function delay(ms) {
619
+ await new Promise((resolve) => setTimeout(resolve, ms));
620
+ }
621
+ async function writeFileAtomically(target, contents, options = {}) {
622
+ const encoding = options.encoding || "utf8";
623
+ const attempts = Math.max(1, Number(options.attempts) || 5);
624
+ const temp = path3.join(os.tmpdir(), `opencode-loop-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`);
625
+ await fs.writeFile(temp, contents, encoding);
626
+ try {
627
+ let lastError;
628
+ for (let attempt = 0;attempt < attempts; attempt++) {
629
+ try {
630
+ await fs.rename(temp, target);
631
+ return;
632
+ } catch (error) {
633
+ lastError = error;
634
+ if (error?.code === "EXDEV")
635
+ break;
636
+ if (!isRetriableStateWriteError(error))
637
+ throw error;
638
+ if (attempt < attempts - 1)
639
+ await delay(25 * (attempt + 1));
640
+ }
641
+ }
642
+ for (let attempt = 0;attempt < attempts; attempt++) {
643
+ try {
644
+ await fs.copyFile(temp, target);
645
+ return;
646
+ } catch (error) {
647
+ lastError = error;
648
+ if (!isRetriableStateWriteError(error))
649
+ throw error;
650
+ if (attempt < attempts - 1)
651
+ await delay(25 * (attempt + 1));
652
+ }
653
+ }
654
+ try {
655
+ await fs.writeFile(target, contents, encoding);
656
+ return;
657
+ } catch (error) {
658
+ if (lastError && !error.cause)
659
+ error.cause = lastError;
660
+ throw error;
661
+ }
662
+ } finally {
663
+ try {
664
+ await fs.rm(temp, { force: true });
665
+ } catch {}
666
+ }
667
+ }
668
+ function stateValuesEqual(left, right) {
669
+ if (Object.is(left, right))
670
+ return true;
671
+ try {
672
+ return JSON.stringify(left) === JSON.stringify(right);
673
+ } catch {
674
+ return false;
675
+ }
676
+ }
677
+ function mergeStateJob(baseJob, intendedJob, currentJob) {
678
+ const merged = structuredClone(currentJob || {});
679
+ const keys = new Set([
680
+ ...Object.keys(baseJob || {}),
681
+ ...Object.keys(intendedJob || {})
682
+ ]);
683
+ for (const key of keys) {
684
+ const baseHas = Object.prototype.hasOwnProperty.call(baseJob || {}, key);
685
+ const intendedHas = Object.prototype.hasOwnProperty.call(intendedJob || {}, key);
686
+ const currentHas = Object.prototype.hasOwnProperty.call(currentJob || {}, key);
687
+ const intendedChanged = baseHas !== intendedHas || !stateValuesEqual(baseJob?.[key], intendedJob?.[key]);
688
+ if (!intendedChanged)
689
+ continue;
690
+ const currentChanged = baseHas !== currentHas || !stateValuesEqual(baseJob?.[key], currentJob?.[key]);
691
+ const sameResult = intendedHas === currentHas && stateValuesEqual(intendedJob?.[key], currentJob?.[key]);
692
+ if (currentChanged && !sameResult)
693
+ continue;
694
+ if (intendedHas)
695
+ merged[key] = structuredClone(intendedJob[key]);
696
+ else
697
+ delete merged[key];
698
+ }
699
+ return merged;
700
+ }
701
+ function mergeStateJobs(baseJobs, intendedJobs, currentJobs) {
702
+ const byID = (jobs) => new Map((jobs || []).filter((job) => job?.id).map((job) => [job.id, job]));
703
+ const base = byID(baseJobs);
704
+ const intended = byID(intendedJobs);
705
+ const current = byID(currentJobs);
706
+ const merged = [];
707
+ for (const currentJob of currentJobs || []) {
708
+ const id = currentJob?.id;
709
+ if (!id || !base.has(id)) {
710
+ merged.push(structuredClone(currentJob));
711
+ continue;
712
+ }
713
+ const baseJob = base.get(id);
714
+ const intendedJob = intended.get(id);
715
+ if (!intendedJob) {
716
+ if (!stateValuesEqual(baseJob, currentJob))
717
+ merged.push(structuredClone(currentJob));
718
+ continue;
719
+ }
720
+ merged.push(mergeStateJob(baseJob, intendedJob, currentJob));
721
+ }
722
+ for (const intendedJob of intendedJobs || []) {
723
+ const id = intendedJob?.id;
724
+ if (!id || base.has(id) || current.has(id))
725
+ continue;
726
+ merged.push(structuredClone(intendedJob));
727
+ }
728
+ return merged;
729
+ }
730
+ async function writeState(directory, sessionID, state) {
731
+ await withStateWriteLock(directory, sessionID, async () => {
732
+ await ensureDir(stateDir(directory));
733
+ const target = statePath(directory, sessionID);
734
+ const baseline = state?.[STATE_BASELINE];
735
+ let jobs = structuredClone(state.jobs || []);
736
+ if (Array.isArray(baseline)) {
737
+ const current = await readStateFile(directory, sessionID);
738
+ jobs = mergeStateJobs(baseline, jobs, current.jobs || []);
739
+ state.jobs = structuredClone(jobs);
740
+ state[STATE_BASELINE] = structuredClone(jobs);
741
+ }
742
+ const payload = JSON.stringify({ version: 4, jobs }, null, 2);
743
+ await writeFileAtomically(target, payload);
744
+ });
745
+ }
746
+ async function removeState(directory, sessionID) {
747
+ await withStateWriteLock(directory, sessionID, async () => {
748
+ try {
749
+ await fs.unlink(statePath(directory, sessionID));
750
+ } catch {}
751
+ });
752
+ }
753
+ var STATE_DIR = ".opencode/opencode-loop", STATE_BASELINE, stateWriteLocks;
754
+ var init_state = __esm(() => {
755
+ STATE_BASELINE = Symbol("opencode-loop-state-baseline");
756
+ stateWriteLocks = new Map;
757
+ });
758
+
759
+ // src/source/core/process.js
760
+ import { promises as fs2 } from "fs";
761
+ import path4 from "path";
762
+ import { spawn as spawn2 } from "child_process";
763
+ async function appendLoopLog(directory, line, extra = {}) {
764
+ try {
765
+ await ensureDir(stateDir(directory));
766
+ await fs2.appendFile(path4.join(stateDir(directory), "loop.log"), JSON.stringify({ time: new Date().toISOString(), line, ...extra }) + `
767
+ `);
768
+ } catch {}
769
+ }
770
+ async function readSmallTextFile(filePath, maxBytes = 120000) {
771
+ try {
772
+ const stat = await fs2.stat(filePath);
773
+ if (!stat.isFile() || stat.size > maxBytes)
774
+ return "";
775
+ return await fs2.readFile(filePath, "utf8");
776
+ } catch {
777
+ return "";
778
+ }
779
+ }
780
+ async function runProcess(command, args, cwd, timeoutMs = 60000) {
781
+ return await new Promise((resolve) => {
782
+ const child = spawn2(command, args, { cwd, shell: false, windowsHide: true });
783
+ const stdout = [];
784
+ const stderr = [];
785
+ let timedOut = false;
786
+ const timer = setTimeout(() => {
787
+ timedOut = true;
788
+ try {
789
+ child.kill("SIGTERM");
790
+ } catch {}
791
+ }, timeoutMs);
792
+ child.stdout?.on("data", (data) => stdout.push(Buffer.from(data)));
793
+ child.stderr?.on("data", (data) => stderr.push(Buffer.from(data)));
794
+ child.on("error", (error) => {
795
+ clearTimeout(timer);
796
+ resolve({ code: -1, stdout: "", stderr: String(error) });
797
+ });
798
+ child.on("close", (code) => {
799
+ clearTimeout(timer);
800
+ resolve({ code: timedOut ? 124 : code ?? -1, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
801
+ });
802
+ });
803
+ }
804
+ async function runShellCommand(command, cwd, timeoutMs = 120000) {
805
+ return await new Promise((resolve) => {
806
+ const child = spawn2(command, [], { cwd, shell: true, windowsHide: true });
807
+ const stdout = [];
808
+ const stderr = [];
809
+ let timedOut = false;
810
+ const timer = setTimeout(() => {
811
+ timedOut = true;
812
+ try {
813
+ child.kill("SIGTERM");
814
+ } catch {}
815
+ }, timeoutMs);
816
+ child.stdout?.on("data", (data) => stdout.push(Buffer.from(data)));
817
+ child.stderr?.on("data", (data) => stderr.push(Buffer.from(data)));
818
+ child.on("error", (error) => {
819
+ clearTimeout(timer);
820
+ resolve({ code: -1, stdout: "", stderr: String(error) });
821
+ });
822
+ child.on("close", (code) => {
823
+ clearTimeout(timer);
824
+ resolve({ code: timedOut ? 124 : code ?? -1, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
825
+ });
826
+ });
827
+ }
828
+ var init_process = __esm(() => {
829
+ init_state();
830
+ });
831
+
832
+ // src/source/runtime/goal-report.js
833
+ var init_goal_report = __esm(() => {
834
+ init_jobs();
835
+ init_state();
836
+ });
837
+ // src/source/runtime/goal-prompt.js
838
+ import path5 from "path";
839
+ async function buildGoalPrompt(directory, job) {
840
+ const sections = [];
841
+ sections.push(`Working directory:
842
+ ${path5.resolve(directory)}
843
+ Keep every file operation inside this directory. Prefer workspace-relative paths such as "src/index.js"; never turn a relative path into a root path such as "/src/index.js".`);
844
+ const objective = String(job.action || "").trim();
845
+ if (objective)
846
+ sections.push(`Goal objective:
847
+ ${objective}`);
848
+ if (job.goalFile) {
849
+ const text = await readSmallTextFile(path5.resolve(directory, job.goalFile), 120000);
850
+ if (text.trim())
851
+ sections.push(`Goal file ${job.goalFile}:
852
+ ${text.trim()}`);
853
+ else
854
+ sections.push(`Goal file ${job.goalFile} was requested but could not be read. Continue from the inline goal objective.`);
855
+ }
856
+ if (job.promptFile) {
857
+ const text = await readSmallTextFile(path5.resolve(directory, job.promptFile), 120000);
858
+ if (text.trim())
859
+ sections.push(`Extra goal instructions from ${job.promptFile}:
860
+ ${text.trim()}`);
861
+ }
862
+ if (job.goalAcceptance?.length)
863
+ sections.push(`Acceptance criteria:
864
+ ` + job.goalAcceptance.map((item, index) => `${index + 1}. ${item}`).join(`
865
+ `));
866
+ if (job.goalChecks?.length)
867
+ sections.push(`Verification commands that define useful evidence:
868
+ ` + job.goalChecks.map((item, index) => `${index + 1}. ${item}`).join(`
869
+ `));
870
+ if (job.verifyCommand)
871
+ sections.push(`Post-turn verify command configured by the loop: ${job.verifyCommand}`);
872
+ if (job.lastGoalChecks?.length)
873
+ sections.push(`Latest goal check results:
874
+ ` + job.lastGoalChecks.map((item) => `- ${item.command}: exit ${item.code}`).join(`
875
+ `));
876
+ if (job.lastVerifyFailure)
877
+ sections.push(`Previous verify/check failure summary:
878
+ ` + String(job.lastVerifyFailure).slice(0, 1600));
879
+ if (job.goalCompletionRejectedReason)
880
+ sections.push(`Previous completion attempt was rejected:
881
+ ${job.goalCompletionRejectedReason}`);
882
+ if ((job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS) > 0)
883
+ sections.push(`No-progress guard:
884
+ ${job.noProgressCount || 0}/${job.maxNoProgress ?? DEFAULT_GOAL_MAX_NO_PROGRESS} recent turn(s) without recorded meaningful progress.`);
885
+ if (job.goalProgress?.length)
886
+ sections.push(`Recent goal progress:
887
+ ` + job.goalProgress.slice(-5).map((item) => `- ${item.time}: ${item.summary}`).join(`
888
+ `));
889
+ for (const file of job.includeFiles || []) {
890
+ const text = await readSmallTextFile(path5.resolve(directory, file), 80000);
891
+ if (text.trim())
892
+ sections.push(`Context from ${file}:
893
+ ${text.trim().slice(0, 20000)}`);
894
+ }
895
+ return `${GOAL_PROMPT_PREFIX}.
896
+
897
+ You are pursuing an experimental persistent goal for this OpenCode session. This is not a timer loop and not a one-shot prompt. Keep working toward the goal until it is completed, blocked, paused, cleared, or stopped by safety limits.
898
+
899
+ Rules:
900
+ - Work on the next smallest useful step toward the goal.
901
+ - Prefer direct code changes, tests, typechecks, builds, and evidence over discussion.
902
+ - Do not claim the goal is complete unless the acceptance criteria are satisfied and verification evidence supports it.
903
+ - If verification commands are configured, do not call opencode_loop_goal_complete until the latest relevant checks have passed unless the user explicitly overrides the goal.
904
+ - Completion evidence must be concrete: mention commands, files, checks, results, or code inspection details.
905
+ - When the goal is complete, call the tool opencode_loop_goal_complete with a summary and evidence.
906
+ - If you are truly blocked and need user input, call the tool opencode_loop_goal_blocked with the reason and what is needed.
907
+ - If you made meaningful progress but the goal is not complete, call the tool opencode_loop_goal_progress with the summary and next step.
908
+ - If you cannot make meaningful progress for this turn, call opencode_loop_goal_blocked instead of repeating the same attempt.
909
+ - Do not call completion tools just to be polite; only call them when the state is real.
910
+ - Do not ask the user questions unless blocked; make reasonable assumptions and continue.
911
+ - Follow safety rules: no destructive commands, force pushes, production deploys, production database resets, or deleting user data.
912
+
913
+ ${sections.join(`
914
+
915
+ ---
916
+
917
+ `)}`;
918
+ }
919
+ var GOAL_PROMPT_PREFIX = "EXPERIMENTAL OPENCODE GOAL MODE ITERATION";
920
+ var init_goal_prompt = __esm(() => {
921
+ init_process();
922
+ });
923
+
924
+ // src/source/runtime/goal-runtime.js
925
+ var init_goal_runtime = __esm(() => {
926
+ init_jobs();
927
+ init_state();
928
+ init_process();
929
+ init_goal_report();
930
+ init_goal_prompt();
931
+ init_goal_report();
932
+ });
933
+
934
+ // src/source/runtime/job-workspace.js
935
+ import { promises as fs3 } from "fs";
936
+ import path6 from "path";
937
+ function requireFunction(value, name) {
938
+ if (typeof value !== "function")
939
+ throw new TypeError(`createJobWorkspaceRuntime requires ${name}`);
940
+ return value;
941
+ }
942
+ function dangerousShell(command) {
943
+ const text = String(command || "").toLowerCase();
944
+ return [
945
+ /\brm\b(?=[^\r\n]*\s-{1,2}(?:[a-z]*r[a-z]*|recursive)\b)(?=[^\r\n]*\s-{1,2}(?:[a-z]*f[a-z]*|force)\b)/,
946
+ /\bremove-item\b[^\r\n]*(?:-recurse|-force)/,
947
+ /\bgit\s+reset\b/,
948
+ /\bgit\s+clean\b/,
949
+ /\bgit\s+push\b/,
950
+ /\bdel\b[^\r\n]*\s\/s\b/,
951
+ /\b(?:rmdir|rd)\b[^\r\n]*\s\/s\b/,
952
+ /(?:^|[;&|]\s*)format(?:\.com)?\s+(?:[a-z]:|\/(?:fs|q)\b)/,
953
+ /\bterraform\s+destroy\b/,
954
+ /\bkubectl\s+delete\b/,
955
+ /\bdeploy\b.*\bproduction\b/
956
+ ].some((pattern) => pattern.test(text));
957
+ }
958
+ function createJobWorkspaceRuntime(options = {}) {
959
+ const toast = requireFunction(options.toast, "toast");
960
+ const runProcess2 = typeof options.runProcess === "function" ? options.runProcess : runProcess;
961
+ const appendLoopLog2 = typeof options.appendLoopLog === "function" ? options.appendLoopLog : appendLoopLog;
962
+ const readSmallTextFile2 = typeof options.readSmallTextFile === "function" ? options.readSmallTextFile : readSmallTextFile;
963
+ const buildGoalPrompt2 = typeof options.buildGoalPrompt === "function" ? options.buildGoalPrompt : buildGoalPrompt;
964
+ async function buildPrompt(directory, job) {
965
+ if (isGoalJob(job))
966
+ return await buildGoalPrompt2(directory, job);
967
+ const sections = [];
968
+ if (job.promptFile) {
969
+ const text = await readSmallTextFile2(path6.resolve(directory, job.promptFile));
970
+ if (text.trim())
971
+ sections.push(`Instructions from ${job.promptFile}:
972
+ ${text.trim()}`);
973
+ else
974
+ sections.push(`Prompt file ${job.promptFile} was requested but could not be read. Continue from the regular action instead.`);
975
+ }
976
+ if (job.action)
977
+ sections.push(decoratePrompt(job));
978
+ for (const file of job.includeFiles || []) {
979
+ const text = await readSmallTextFile2(path6.resolve(directory, file), 80000);
980
+ if (text.trim())
981
+ sections.push(`Context from ${file}:
982
+ ${text.trim().slice(0, 20000)}`);
983
+ }
984
+ return sections.join(`
985
+
986
+ ---
987
+
988
+ `) || decoratePrompt(job);
989
+ }
990
+ async function ensureBranch(directory, job, client, sessionID) {
991
+ if (!job.branch || job.branchDone)
992
+ return job;
993
+ const branch = safeID(job.branch);
994
+ const inRepo = await runProcess2("git", ["rev-parse", "--is-inside-work-tree"], directory, 1e4);
995
+ if (inRepo.code !== 0) {
996
+ job.branchDone = false;
997
+ job.branchUnavailable = true;
998
+ return job;
999
+ }
1000
+ let result = await runProcess2("git", ["switch", branch], directory, 30000);
1001
+ if (result.code !== 0)
1002
+ result = await runProcess2("git", ["switch", "-c", branch], directory, 30000);
1003
+ job.branchDone = result.code === 0;
1004
+ await toast(client, result.code === 0 ? `Loop branch active: ${branch}` : `Could not switch/create branch: ${branch}`, result.code === 0 ? "success" : "warning");
1005
+ await appendLoopLog2(directory, "branch", { sessionID, branch, code: result.code });
1006
+ return job;
1007
+ }
1008
+ async function snapshotPaths(directory, files) {
1009
+ const snapshot = {};
1010
+ for (const file of files || []) {
1011
+ try {
1012
+ const stat = await fs3.stat(path6.resolve(directory, file));
1013
+ snapshot[file] = `${stat.mtimeMs}:${stat.size}`;
1014
+ } catch {
1015
+ snapshot[file] = "missing";
1016
+ }
1017
+ }
1018
+ return snapshot;
1019
+ }
1020
+ async function watchChanged(directory, job) {
1021
+ if (!job.watchPaths?.length)
1022
+ return false;
1023
+ const next = await snapshotPaths(directory, job.watchPaths);
1024
+ const previous = job.watchSnapshot || {};
1025
+ const changed = job.watchPaths.some((file) => previous[file] !== next[file]);
1026
+ if (changed)
1027
+ job.watchSnapshot = next;
1028
+ return changed;
1029
+ }
1030
+ async function fileContains(filePath, needle) {
1031
+ try {
1032
+ const stat = await fs3.stat(filePath);
1033
+ if (!stat.isFile() || stat.size > MAX_SCAN_BYTES)
1034
+ return false;
1035
+ return (await fs3.readFile(filePath, "utf8")).includes(needle);
1036
+ } catch {
1037
+ return false;
1038
+ }
1039
+ }
1040
+ async function untilReached(directory, job) {
1041
+ if (!job.until)
1042
+ return false;
1043
+ const controlRoots = new Set([
1044
+ stateDir(directory),
1045
+ ...["goals", "goal-locks", "goal-handoff-locks", "goal-sequences"].map((name) => path6.join(directory, ".opencode", name))
1046
+ ].map((root) => path6.resolve(root).toLowerCase()));
1047
+ const files = ["progress.md", "PROGRESS.md", "todo.md", "TODO.md", "todolist.md", "TODOLIST.md", path6.join(".opencode", "opencode-loop", "until.txt")];
1048
+ for (const file of files)
1049
+ if (await fileContains(path6.resolve(directory, file), job.until))
1050
+ return true;
1051
+ let scanned = 0;
1052
+ async function walk(current) {
1053
+ if (scanned >= MAX_SCAN_FILES)
1054
+ return false;
1055
+ let entries;
1056
+ try {
1057
+ entries = await fs3.readdir(current, { withFileTypes: true });
1058
+ } catch {
1059
+ return false;
1060
+ }
1061
+ for (const entry of entries) {
1062
+ if (scanned >= MAX_SCAN_FILES)
1063
+ return false;
1064
+ if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name))
1065
+ continue;
1066
+ const full = path6.join(current, entry.name);
1067
+ if (controlRoots.has(path6.resolve(full).toLowerCase()))
1068
+ continue;
1069
+ if (entry.isDirectory()) {
1070
+ if (await walk(full))
1071
+ return true;
1072
+ } else if (entry.isFile() && /\.(md|txt|json|yaml|yml)$/i.test(entry.name)) {
1073
+ scanned++;
1074
+ if (await fileContains(full, job.until))
1075
+ return true;
1076
+ }
1077
+ }
1078
+ return false;
1079
+ }
1080
+ return await walk(directory);
1081
+ }
1082
+ async function createCheckpoint(directory, sessionID, job, client) {
1083
+ if (!job.checkpointOnly && !job.gitCheckpoint)
1084
+ return;
1085
+ const inRepo = await runProcess2("git", ["rev-parse", "--is-inside-work-tree"], directory, 1e4);
1086
+ if (inRepo.code !== 0)
1087
+ return;
1088
+ const status = await runProcess2("git", ["status", "--short"], directory, 30000);
1089
+ if (!status.stdout.trim())
1090
+ return;
1091
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1092
+ const checkpointDir = path6.join(stateDir(directory), "checkpoints", safeID(sessionID));
1093
+ await ensureDir(checkpointDir);
1094
+ const diff = await runProcess2("git", ["diff", "--binary"], directory, 120000);
1095
+ const staged = await runProcess2("git", ["diff", "--cached", "--binary"], directory, 120000);
1096
+ const prefix = `${timestamp}-${safeID(job.name || job.id)}`;
1097
+ await fs3.writeFile(path6.join(checkpointDir, `${prefix}.status.txt`), status.stdout + status.stderr);
1098
+ await fs3.writeFile(path6.join(checkpointDir, `${prefix}.patch`), `${diff.stdout}
1099
+ ${staged.stdout}`);
1100
+ if (job.gitCheckpoint) {
1101
+ await runProcess2("git", ["add", "-A"], directory, 120000);
1102
+ await runProcess2("git", ["commit", "-m", `chore: opencode loop checkpoint ${timestamp}`], directory, 120000);
1103
+ }
1104
+ await toast(client, `Loop checkpoint saved: ${prefix}`, "success");
1105
+ }
1106
+ return {
1107
+ buildPrompt,
1108
+ ensureBranch,
1109
+ snapshotPaths,
1110
+ watchChanged,
1111
+ untilReached,
1112
+ createCheckpoint
1113
+ };
1114
+ }
1115
+ var MAX_SCAN_FILES = 200, MAX_SCAN_BYTES = 2000000;
1116
+ var init_job_workspace = __esm(() => {
1117
+ init_jobs();
1118
+ init_state();
1119
+ init_process();
1120
+ init_goal_runtime();
1121
+ });
1122
+
1123
+ // src/source/opencode2/native-plugin.js
1124
+ import path9 from "path";
1125
+
1126
+ // src/source/opencode2/session-location.js
1127
+ import path from "path";
1128
+ function createNativeSessionLocationGuard({ directory, workspaceID, getSession }) {
1129
+ const expected = path.resolve(directory);
1130
+ return async (sessionID) => {
1131
+ if (typeof sessionID !== "string" || !sessionID || typeof getSession !== "function")
1132
+ return false;
1133
+ try {
1134
+ const session = await getSession({ sessionID });
1135
+ if (session?.id !== sessionID || typeof session.location?.directory !== "string" || !path.isAbsolute(session.location.directory))
1136
+ return false;
1137
+ if (path.relative(expected, path.resolve(session.location.directory)) !== "")
1138
+ return false;
1139
+ return (session.location.workspaceID ?? "") === (workspaceID ?? "");
1140
+ } catch {
1141
+ return false;
1142
+ }
1143
+ };
1144
+ }
1145
+
1146
+ // src/source/opencode2/native-shell.js
1147
+ import { spawn } from "child_process";
1148
+ import { randomUUID } from "crypto";
1149
+ function createNativeShellHost({ directory, onTerminal, onError = () => {} } = {}) {
1150
+ if (!directory || typeof onTerminal !== "function")
1151
+ throw new TypeError("Native shell requires directory and onTerminal");
1152
+ const tasks = new Set;
1153
+ let disposed = false;
1154
+ let disposal;
1155
+ const report = (error) => {
1156
+ try {
1157
+ onError(error);
1158
+ } catch {}
1159
+ };
1160
+ const tail = (text, data) => (text + String(data)).slice(-64000);
1161
+ function terminate(task) {
1162
+ if (task.termination)
1163
+ return task.termination;
1164
+ if (task.settled || !task.child.pid)
1165
+ return Promise.resolve();
1166
+ const pid = task.child.pid;
1167
+ task.termination = new Promise((resolve) => {
1168
+ if (process.platform === "win32") {
1169
+ const fallback = () => {
1170
+ try {
1171
+ task.child.kill();
1172
+ } catch {}
1173
+ };
1174
+ try {
1175
+ const killer = spawn("taskkill", ["/pid", String(pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" });
1176
+ killer.once("error", (error) => {
1177
+ report(error);
1178
+ fallback();
1179
+ resolve();
1180
+ });
1181
+ killer.once("close", (code) => {
1182
+ if (code !== 0)
1183
+ fallback();
1184
+ resolve();
1185
+ });
1186
+ } catch (error) {
1187
+ report(error);
1188
+ fallback();
1189
+ resolve();
1190
+ }
1191
+ } else {
1192
+ try {
1193
+ process.kill(-pid, "SIGTERM");
1194
+ } catch {
1195
+ try {
1196
+ task.child.kill("SIGTERM");
1197
+ } catch {}
1198
+ }
1199
+ setTimeout(() => {
1200
+ try {
1201
+ process.kill(-pid, "SIGKILL");
1202
+ } catch {}
1203
+ resolve();
1204
+ }, 1000);
1205
+ }
1206
+ });
1207
+ return task.termination;
1208
+ }
1209
+ async function dispatch(request) {
1210
+ if (disposed)
1211
+ throw new Error("Native shell is disposed");
1212
+ if (!request?.sessionID || typeof request.command !== "string" || !request.command.trim())
1213
+ throw new TypeError("Native shell requires sessionID and command");
1214
+ const shellID = request.id || `shell_loop_${randomUUID().replaceAll("-", "")}`;
1215
+ const child = spawn(request.command, { cwd: directory, shell: true, detached: process.platform !== "win32", windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
1216
+ const task = { child, settled: false, timedOut: false, cancelled: false, stdout: "", stderr: "" };
1217
+ task.closed = new Promise((resolve) => {
1218
+ task.resolve = resolve;
1219
+ });
1220
+ tasks.add(task);
1221
+ child.stdout?.on("data", (chunk) => {
1222
+ task.stdout = tail(task.stdout, chunk);
1223
+ });
1224
+ child.stderr?.on("data", (chunk) => {
1225
+ task.stderr = tail(task.stderr, chunk);
1226
+ });
1227
+ const delay = Number(request.timeoutMs);
1228
+ task.timer = setTimeout(() => {
1229
+ task.timedOut = true;
1230
+ terminate(task);
1231
+ }, Number.isFinite(delay) && delay > 0 ? Math.min(delay, 2147483647) : 120000);
1232
+ task.timer.unref?.();
1233
+ child.once("close", async (code, signal) => {
1234
+ task.settled = true;
1235
+ clearTimeout(task.timer);
1236
+ await task.termination;
1237
+ tasks.delete(task);
1238
+ const event = {
1239
+ kind: "shell",
1240
+ action: "ended",
1241
+ directory,
1242
+ sessionID: request.sessionID,
1243
+ shellID,
1244
+ status: task.timedOut ? "timeout" : task.cancelled || signal || code === null ? "killed" : "exited",
1245
+ code: task.timedOut ? 124 : Number.isInteger(code) ? code : -1,
1246
+ stdout: task.stdout,
1247
+ stderr: task.stderr
1248
+ };
1249
+ task.resolve(event);
1250
+ if (!task.spawnError)
1251
+ Promise.resolve().then(() => onTerminal(event)).catch(report);
1252
+ });
1253
+ return new Promise((resolve, reject) => {
1254
+ child.once("spawn", () => resolve({ id: shellID, status: "running" }));
1255
+ child.once("error", (error) => {
1256
+ task.spawnError = true;
1257
+ reject(error);
1258
+ });
1259
+ });
1260
+ }
1261
+ function dispose() {
1262
+ if (disposal)
1263
+ return disposal;
1264
+ disposed = true;
1265
+ const active = [...tasks];
1266
+ for (const task of active) {
1267
+ task.cancelled = true;
1268
+ terminate(task);
1269
+ }
1270
+ disposal = Promise.allSettled(active.map((task) => task.closed)).then(() => {
1271
+ return;
1272
+ });
1273
+ return disposal;
1274
+ }
1275
+ return Object.freeze({ dispatch, dispose, activeCount: () => tasks.size });
1276
+ }
1277
+
1278
+ // src/source/opencode2/native-runtime.js
1279
+ import { randomUUID as randomUUID2 } from "crypto";
1280
+
1281
+ // src/source/opencode2/native-companion.js
1282
+ import { createHash } from "crypto";
1283
+ import { lstat, readFile } from "fs/promises";
1284
+ import path2 from "path";
1285
+ async function nativeGoalReservesSession(directory, sessionID) {
1286
+ const shard = createHash("sha256").update(sessionID).digest("hex").slice(0, 32);
1287
+ const root = path2.resolve(directory);
1288
+ const parts = [".opencode", "goals", `${shard}.json`];
1289
+ let file = root;
1290
+ try {
1291
+ for (const part of parts) {
1292
+ file = path2.join(file, part);
1293
+ const info = await lstat(file);
1294
+ if (info.isSymbolicLink())
1295
+ return true;
1296
+ }
1297
+ const goal = JSON.parse(await readFile(file, "utf8"));
1298
+ if (goal?.schemaVersion !== 1 || goal?.sessionID !== sessionID || typeof goal?.id !== "string")
1299
+ return true;
1300
+ return !["completed", "cleared"].includes(goal.status);
1301
+ } catch (error) {
1302
+ return error?.code !== "ENOENT";
1303
+ }
1304
+ }
1305
+
1306
+ // src/source/opencode2/native-runtime.js
1307
+ init_jobs();
1308
+
1309
+ // src/source/core/schedule-syntax.js
1310
+ function removeBooleanFlag(input, flag) {
1311
+ const pattern = new RegExp(`(^|\\s)${flag.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}(?=\\s|$)`, "i");
1312
+ const found = pattern.test(input);
1313
+ return {
1314
+ found,
1315
+ value: String(input || "").replace(pattern, " ").replace(/\s+/g, " ").trim()
1316
+ };
1317
+ }
1318
+ function firstToken(input) {
1319
+ const match = String(input || "").trim().match(/^(\S+)(?:\s+([\s\S]*))?$/);
1320
+ return match ? { token: match[1], rest: String(match[2] || "").trim() } : { token: "", rest: "" };
1321
+ }
1322
+ function inferredMode(intervalMs, maxRuns) {
1323
+ if (Number(maxRuns || 0) === 1 && Number(intervalMs || 0) > 0)
1324
+ return "once";
1325
+ return Number(intervalMs || 0) === 0 ? "idle" : "interval";
1326
+ }
1327
+ function normalizeLoopScheduleArgs(raw, defaults = {}) {
1328
+ const overlap = removeBooleanFlag(String(raw || "").trim(), "--allow-goal-overlap");
1329
+ let input = overlap.value;
1330
+ const nextDefaults = { ...defaults };
1331
+ let scheduleMode = defaults.scheduleMode;
1332
+ let scheduleSyntax = "legacy";
1333
+ const first = firstToken(input);
1334
+ const keyword = first.token.toLowerCase();
1335
+ if (keyword === "idle") {
1336
+ nextDefaults.intervalMs = 0;
1337
+ nextDefaults.immediate = true;
1338
+ scheduleMode = "idle";
1339
+ scheduleSyntax = "idle";
1340
+ input = first.rest;
1341
+ } else if (keyword === "every" || keyword === "after" || keyword === "in") {
1342
+ const duration = firstToken(first.rest);
1343
+ const intervalMs = parseDuration(duration.token);
1344
+ if (intervalMs === null) {
1345
+ return {
1346
+ ok: false,
1347
+ error: `Invalid ${keyword} schedule. Example: /loop ${keyword === "every" ? "every" : "after"} 5m continue the project`
1348
+ };
1349
+ }
1350
+ nextDefaults.intervalMs = intervalMs;
1351
+ nextDefaults.immediate = false;
1352
+ input = duration.rest;
1353
+ if (keyword === "every") {
1354
+ scheduleMode = intervalMs === 0 ? "idle" : "interval";
1355
+ scheduleSyntax = "every";
1356
+ } else {
1357
+ nextDefaults.maxRuns = 1;
1358
+ scheduleMode = "once";
1359
+ scheduleSyntax = "after";
1360
+ }
1361
+ } else {
1362
+ const duration = parseDuration(first.token);
1363
+ if (duration !== null) {
1364
+ scheduleMode = duration === 0 ? "idle" : "interval";
1365
+ } else if (nextDefaults.intervalMs === undefined || nextDefaults.intervalMs === null) {
1366
+ nextDefaults.intervalMs = 0;
1367
+ nextDefaults.immediate = nextDefaults.immediate ?? true;
1368
+ scheduleMode = "idle";
1369
+ scheduleSyntax = "idle-shorthand";
1370
+ } else {
1371
+ scheduleMode = scheduleMode || inferredMode(nextDefaults.intervalMs, nextDefaults.maxRuns);
1372
+ }
1373
+ }
1374
+ return {
1375
+ ok: true,
1376
+ args: input,
1377
+ defaults: nextDefaults,
1378
+ scheduleMode: scheduleMode || inferredMode(nextDefaults.intervalMs, nextDefaults.maxRuns),
1379
+ scheduleSyntax,
1380
+ allowGoalOverlap: overlap.found || defaults.allowGoalOverlap === true
1381
+ };
1382
+ }
1383
+
1384
+ // src/source/opencode2/commands.js
1385
+ var OPENCODE_LOOP_V2_PRESET_NAMES = Object.freeze([
1386
+ "loop-dev",
1387
+ "loop-testfix",
1388
+ "loop-compact",
1389
+ "loop-progress",
1390
+ "loop-safe-dev",
1391
+ "loop-command",
1392
+ "loop-cmd",
1393
+ "loop-prompt",
1394
+ "loop-ask",
1395
+ "loop-shell"
1396
+ ]);
1397
+ var OPENCODE_LOOP_V2_COMMANDS = Object.freeze({
1398
+ loop: Object.freeze({
1399
+ description: "Start an OpenCode auto-continue loop. Usage: /loop 5m <task>",
1400
+ template: "OpenCode Loop local command handled. Reply exactly: OK."
1401
+ }),
1402
+ "loop-pause": Object.freeze({
1403
+ description: "Pause OpenCode Loop jobs.",
1404
+ template: "OpenCode Loop pause command handled locally. Reply exactly: OK."
1405
+ }),
1406
+ "loop-resume": Object.freeze({
1407
+ description: "Resume OpenCode Loop jobs.",
1408
+ template: "OpenCode Loop resume command handled locally. Reply exactly: OK."
1409
+ }),
1410
+ "loop-stop": Object.freeze({
1411
+ description: "Stop OpenCode Loop jobs.",
1412
+ template: "OpenCode Loop stop command handled locally. Reply exactly: OK."
1413
+ }),
1414
+ "loop-remove": Object.freeze({
1415
+ description: "Remove OpenCode Loop jobs.",
1416
+ template: "OpenCode Loop remove command handled locally. Reply exactly: OK."
1417
+ }),
1418
+ "loop-clear": Object.freeze({
1419
+ description: "Clear all OpenCode Loop jobs.",
1420
+ template: "OpenCode Loop clear command handled locally. Reply exactly: OK."
1421
+ }),
1422
+ "loop-status": Object.freeze({
1423
+ description: "Show OpenCode Loop status for the current session.",
1424
+ template: "OpenCode Loop status command handled locally. Reply exactly: OK."
1425
+ }),
1426
+ "loop-now": Object.freeze({
1427
+ description: "Run matching OpenCode Loop jobs on the next idle boundary.",
1428
+ template: "OpenCode Loop run-now command handled locally. Reply exactly: OK."
1429
+ }),
1430
+ "loop-export": Object.freeze({
1431
+ description: "Export OpenCode Loop state for the current session.",
1432
+ template: "OpenCode Loop export command handled locally. Reply exactly: OK."
1433
+ }),
1434
+ "loop-help": Object.freeze({
1435
+ description: "Show the native OpenCode 2 Loop command help.",
1436
+ template: "OpenCode Loop help command handled locally. Reply exactly: OK."
1437
+ }),
1438
+ "loop-doctor": Object.freeze({
1439
+ description: "Show native OpenCode 2 Loop diagnostics.",
1440
+ template: "OpenCode Loop doctor command handled locally. Reply exactly: OK."
1441
+ }),
1442
+ "loop-logs": Object.freeze({
1443
+ description: "Show recent native OpenCode 2 Loop runtime events.",
1444
+ template: "OpenCode Loop logs command handled locally. Reply exactly: OK."
1445
+ })
1446
+ });
1447
+ function commandArguments(input) {
1448
+ const prompt = input?.prompt;
1449
+ if (typeof prompt?.text === "string")
1450
+ return prompt.text.trim();
1451
+ if (typeof input?.arguments === "string")
1452
+ return input.arguments.trim();
1453
+ return "";
1454
+ }
1455
+ function registerOpenCode2LoopCommands(draft, options = {}) {
1456
+ if (!draft || typeof draft !== "object") {
1457
+ throw new Error("OpenCode 2 command transform draft is unavailable");
1458
+ }
1459
+ if (typeof draft.add === "function") {
1460
+ const execute = options.execute;
1461
+ if (typeof execute !== "function") {
1462
+ throw new Error("OpenCode 2 command draft.add requires a local Loop command executor");
1463
+ }
1464
+ for (const [name, definition] of Object.entries(OPENCODE_LOOP_V2_COMMANDS)) {
1465
+ draft.add({
1466
+ name,
1467
+ description: definition.description,
1468
+ execute: async (input) => await execute({
1469
+ name,
1470
+ sessionID: input?.sessionID,
1471
+ arguments: commandArguments(input),
1472
+ delivery: input?.delivery
1473
+ })
1474
+ });
1475
+ }
1476
+ return;
1477
+ }
1478
+ if (typeof draft.update === "function") {
1479
+ for (const [name, definition] of Object.entries(OPENCODE_LOOP_V2_COMMANDS)) {
1480
+ draft.update(name, (command) => {
1481
+ command.template = definition.template;
1482
+ command.description = definition.description;
1483
+ });
1484
+ }
1485
+ return;
1486
+ }
1487
+ throw new Error("OpenCode 2 command transform exposes neither add() nor update()");
1488
+ }
1489
+ function parseOpenCode2LoopCommandText(value) {
1490
+ if (typeof value !== "string")
1491
+ return;
1492
+ for (const [name, definition] of Object.entries(OPENCODE_LOOP_V2_COMMANDS)) {
1493
+ const template = definition.template;
1494
+ if (value === template)
1495
+ return Object.freeze({ name, arguments: "" });
1496
+ if (!value.startsWith(`${template}
1497
+
1498
+ `))
1499
+ continue;
1500
+ return Object.freeze({
1501
+ name,
1502
+ arguments: value.slice(template.length + 2).trim()
1503
+ });
1504
+ }
1505
+ return;
1506
+ }
1507
+
1508
+ // src/source/opencode2/native-runtime.js
1509
+ init_state();
1510
+ init_process();
1511
+
1512
+ // src/source/opencode2/status.js
1513
+ init_jobs();
1514
+ function dueAt(job, current) {
1515
+ if (Number(job?.runNowRequestedAt || 0) > 0)
1516
+ return current;
1517
+ const intervalMs = Math.max(0, Number(job?.intervalMs || 0));
1518
+ const lastRunAt = Number(job?.lastRunAt || 0);
1519
+ if (intervalMs === 0)
1520
+ return current;
1521
+ if (lastRunAt > 0)
1522
+ return lastRunAt + intervalMs;
1523
+ if (job?.immediate === false) {
1524
+ const createdAt = Date.parse(job?.createdAt || "");
1525
+ return (Number.isFinite(createdAt) ? createdAt : current) + intervalMs;
1526
+ }
1527
+ return current;
1528
+ }
1529
+ function formatOpenCode2LoopStatus(state, current = Date.now()) {
1530
+ const jobs = Array.isArray(state?.jobs) ? state.jobs : [];
1531
+ const lines = jobs.length ? jobs.map((job, index) => {
1532
+ const dueIn = Math.max(0, dueAt(job, current) - current);
1533
+ const flags = [
1534
+ isGoalJob(job) ? `goal:${goalStatusText(job)}` : undefined,
1535
+ job.paused ? "paused" : "active",
1536
+ Number(job.runNowRequestedAt || 0) > 0 ? "run-now" : undefined,
1537
+ job.safe ? "safe" : undefined,
1538
+ job.askNever ? "ask-never" : undefined,
1539
+ job.noOverlap ? "no-overlap" : undefined,
1540
+ job.checkpointOnly ? "checkpoint-only" : undefined,
1541
+ job.gitCheckpoint ? "git-checkpoint" : undefined
1542
+ ].filter(Boolean).join(",");
1543
+ return `${index + 1}. ${job.id}${job.name ? ` (${job.name})` : ""}: ${jobLabel(job)} | runs=${job.runCount || 0} | failures=${job.failureCount || 0} | due in ${durationToText(dueIn)} | ${flags}`;
1544
+ }) : ["No active loop jobs."];
1545
+ return Object.freeze({
1546
+ jobs,
1547
+ text: `OpenCode loop status:
1548
+ ${lines.join(`
1549
+ `)}`
1550
+ });
1551
+ }
1552
+
1553
+ // src/source/opencode2/native-policy.js
1554
+ init_process();
1555
+ init_job_workspace();
1556
+ import { stat } from "fs/promises";
1557
+ import path7 from "path";
1558
+ function createNativeJobPolicy(options = {}) {
1559
+ const now = options.now || Date.now;
1560
+ const run = options.runShellCommand || runShellCommand;
1561
+ const workspace = options.workspace || createJobWorkspaceRuntime({ toast: async () => {} });
1562
+ function pause(job, reason, failed = false) {
1563
+ job.paused = true;
1564
+ job.pauseReason = reason;
1565
+ if (failed)
1566
+ job.failureCount = (job.failureCount || 0) + 1;
1567
+ return false;
1568
+ }
1569
+ async function shell(scope, job, command, phase) {
1570
+ if (job.safe && dangerousShell(command)) {
1571
+ return { code: -1, stdout: "", stderr: `Blocked ${phase} by --safe command guard`, blocked: true };
1572
+ }
1573
+ const result = await run(command, scope.directory, Math.max(1, Number(job.timeoutMs) || 120000));
1574
+ await appendLoopLog(scope.directory, `v2-${phase}`, {
1575
+ sessionID: scope.sessionID,
1576
+ job: job.name || job.id,
1577
+ code: result.code,
1578
+ output: String(result.stdout || "").slice(-8000),
1579
+ error: String(result.stderr || "").slice(-8000)
1580
+ });
1581
+ return result;
1582
+ }
1583
+ async function checkStop(scope, job) {
1584
+ const created = Date.parse(job.createdAt);
1585
+ if (job.maxRuntimeMs > 0 && Number.isFinite(created) && now() - created >= job.maxRuntimeMs) {
1586
+ job.enabled = false;
1587
+ return pause(job, "max-runtime");
1588
+ }
1589
+ if (job.stopFile) {
1590
+ try {
1591
+ await stat(path7.resolve(scope.directory, job.stopFile));
1592
+ job.enabled = false;
1593
+ return pause(job, "stop-file");
1594
+ } catch (error) {
1595
+ if (error?.code !== "ENOENT")
1596
+ throw error;
1597
+ }
1598
+ }
1599
+ if (await workspace.untilReached(scope.directory, job)) {
1600
+ job.enabled = false;
1601
+ return pause(job, "until-reached");
1602
+ }
1603
+ return true;
1604
+ }
1605
+ async function prepare(scope, job, isCurrent = () => true) {
1606
+ if (!await checkStop(scope, job) || !isCurrent())
1607
+ return false;
1608
+ if (job.dryRun)
1609
+ return true;
1610
+ if (job.preflightCommand) {
1611
+ const result = await shell(scope, job, job.preflightCommand, "preflight");
1612
+ job.lastPreflightCode = result.code;
1613
+ if (result.code !== 0)
1614
+ return pause(job, "preflight-failed", true);
1615
+ }
1616
+ if (!isCurrent())
1617
+ return false;
1618
+ if (job.branch) {
1619
+ await workspace.ensureBranch(scope.directory, job, undefined, scope.sessionID);
1620
+ if (!job.branchDone)
1621
+ return pause(job, "branch-unavailable", true);
1622
+ }
1623
+ return isCurrent();
1624
+ }
1625
+ async function finish(scope, job, isCurrent = () => true) {
1626
+ if (!isCurrent() || job.dryRun)
1627
+ return;
1628
+ if (job.verifyCommand) {
1629
+ const result = await shell(scope, job, job.verifyCommand, "verify");
1630
+ job.lastVerifyCode = result.code;
1631
+ job.lastVerifyAt = now();
1632
+ job.lastVerifyOutput = `${result.stdout || ""}
1633
+ ${result.stderr || ""}`.trim().slice(-8000);
1634
+ if (result.code !== 0) {
1635
+ job.failureCount = (job.failureCount || 0) + 1;
1636
+ if (result.blocked || job.pauseOnVerifyFail || job.maxFailures > 0 && job.failureCount >= job.maxFailures)
1637
+ pause(job, "verification-failed");
1638
+ return;
1639
+ }
1640
+ job.failureCount = 0;
1641
+ }
1642
+ if (!isCurrent())
1643
+ return;
1644
+ if (job.postrunCommand) {
1645
+ const result = await shell(scope, job, job.postrunCommand, "postrun");
1646
+ job.lastPostrunCode = result.code;
1647
+ if (result.code !== 0) {
1648
+ pause(job, "postrun-failed", true);
1649
+ return;
1650
+ }
1651
+ }
1652
+ if (isCurrent())
1653
+ await workspace.createCheckpoint(scope.directory, scope.sessionID, job, undefined);
1654
+ }
1655
+ async function notify(scope, job, reason) {
1656
+ if (!job.notifyCommand || job.dryRun)
1657
+ return;
1658
+ const literal = (value) => String(value || "").replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 160);
1659
+ const command = String(job.notifyCommand).replace(/\{reason\}/g, literal(reason)).replace(/\{job\}/g, literal(job.name || job.id));
1660
+ await shell(scope, job, command, "notify");
1661
+ }
1662
+ return {
1663
+ pause,
1664
+ checkStop,
1665
+ prepare,
1666
+ finish,
1667
+ notify,
1668
+ shell,
1669
+ buildPrompt: workspace.buildPrompt,
1670
+ snapshotPaths: workspace.snapshotPaths,
1671
+ watchChanged: workspace.watchChanged
1672
+ };
1673
+ }
1674
+
1675
+ // src/source/opencode2/native-runtime.js
1676
+ var PREFIX = "AUTONOMOUS OPENCODE LOOP ITERATION. Continue the configured task now. Do not explain the /loop command. Work directly on the configured task.";
1677
+ var result = (detail = {}) => ({ handled: true, dispatched: false, ...detail });
1678
+ var id = () => `msg_loop_${randomUUID2().replaceAll("-", "")}`;
1679
+ function createNativeLoopRuntime(options = {}) {
1680
+ if (typeof options.prompt !== "function")
1681
+ throw new TypeError("Native Loop requires prompt()");
1682
+ const now = options.now || Date.now;
1683
+ const setTimer = options.setTimer || setTimeout;
1684
+ const clearTimer = options.clearTimer || clearTimeout;
1685
+ const policy = createNativeJobPolicy({ ...options, now });
1686
+ const scopes = new Map;
1687
+ let disposed = false;
1688
+ function scopeFor(event) {
1689
+ const sessionID = String(event?.sessionID || "").trim();
1690
+ const directory = String(event?.directory || options.directory || "").trim();
1691
+ if (!sessionID || !directory)
1692
+ return;
1693
+ const key = `${directory}\x00${sessionID}`;
1694
+ if (!scopes.has(key))
1695
+ scopes.set(key, { key, sessionID, directory, epoch: 0, busy: false, queue: Promise.resolve() });
1696
+ return scopes.get(key);
1697
+ }
1698
+ const current = (scope) => !disposed && !scope.deleted;
1699
+ const read = (scope) => readState(scope.directory, scope.sessionID);
1700
+ const save = (scope, state) => writeState(scope.directory, scope.sessionID, state);
1701
+ const report = (error) => {
1702
+ try {
1703
+ options.onError?.(error);
1704
+ } catch {}
1705
+ };
1706
+ function enqueue(scope, task) {
1707
+ const pending = scope.queue.catch(() => {}).then(async () => {
1708
+ if (!current(scope))
1709
+ return result({ reason: "disposed" });
1710
+ if (typeof options.scopeAllowed === "function" && !await options.scopeAllowed(scope)) {
1711
+ scope.epoch++;
1712
+ clear(scope, "timer");
1713
+ clear(scope, "deadline");
1714
+ clear(scope, "companionTimer");
1715
+ return result({ accepted: false, reason: "session-location-mismatch", error: "Native session location cannot be verified for this project/worktree." });
1716
+ }
1717
+ return current(scope) ? task() : result({ reason: "disposed" });
1718
+ });
1719
+ scope.queue = pending.catch(report);
1720
+ return pending;
1721
+ }
1722
+ function clear(scope, field) {
1723
+ if (scope[field] !== undefined)
1724
+ clearTimer(scope[field]);
1725
+ delete scope[field];
1726
+ }
1727
+ function eligible(job) {
1728
+ return job.enabled !== false && !job.paused && !(job.maxRuns > 0 && (job.runCount || 0) >= job.maxRuns);
1729
+ }
1730
+ function dueAt(job) {
1731
+ if (job.runNowRequestedAt > 0)
1732
+ return now();
1733
+ if (job.watchPaths?.length && !job.watchTriggered)
1734
+ return Infinity;
1735
+ if (job.lastRunAt > 0)
1736
+ return job.lastRunAt + Math.max(0, job.intervalMs || 0);
1737
+ return job.immediate === false ? Date.parse(job.createdAt) + Math.max(0, job.intervalMs || 0) : now();
1738
+ }
1739
+ function blocked(job) {
1740
+ const kind = actionKind(job.action, job);
1741
+ if (kind === "goal")
1742
+ return ["Use the native @bybrawe/opencode-goal /goal workflow; legacy Loop Goal state is not silently converted."];
1743
+ if (!["prompt", "command", "compact", "shell"].includes(kind))
1744
+ return ["unsupported action kind"];
1745
+ if (kind === "command" && (typeof options.command !== "function" || typeof options.wait !== "function"))
1746
+ return ["native command and wait capabilities are required"];
1747
+ if ((kind === "compact" || job.compactEveryRuns > 0 || job.compactEveryMs > 0) && typeof options.compact !== "function")
1748
+ return ["native compaction capability is required"];
1749
+ if (kind === "shell" && typeof options.shell !== "function")
1750
+ return ["native shell capability is required"];
1751
+ if (job.noOverlap === false)
1752
+ return ["Native V2 sessions are serialized; remove --allow-overlap."];
1753
+ return [];
1754
+ }
1755
+ async function schedule(scope) {
1756
+ clear(scope, "timer");
1757
+ if (!current(scope) || scope.busy || scope.active || scope.compaction)
1758
+ return;
1759
+ const state = await read(scope);
1760
+ if (!current(scope) || scope.busy || scope.active || scope.compaction)
1761
+ return;
1762
+ let delay = Infinity;
1763
+ for (const job of state.jobs || []) {
1764
+ if (!eligible(job) || job.v2Run || blocked(job).length)
1765
+ continue;
1766
+ if (job.watchPaths?.length || job.stopFile || job.until)
1767
+ delay = Math.min(delay, 1000);
1768
+ const due = dueAt(job);
1769
+ if (due > now())
1770
+ delay = Math.min(delay, due - now());
1771
+ else if (job.immediate === false && !(job.lastRunAt > 0))
1772
+ delay = Math.min(delay, 1);
1773
+ if (job.maxRuntimeMs > 0)
1774
+ delay = Math.min(delay, Math.max(1, Date.parse(job.createdAt) + job.maxRuntimeMs - now()));
1775
+ }
1776
+ if (!Number.isFinite(delay))
1777
+ return;
1778
+ scope.timer = setTimer(() => {
1779
+ delete scope.timer;
1780
+ return enqueue(scope, () => advance(scope)).catch(report);
1781
+ }, Math.max(1, Math.min(delay, 2147483647)));
1782
+ scope.timer?.unref?.();
1783
+ }
1784
+ async function note(scope, name, detail = {}) {
1785
+ await appendLoopLog(scope.directory, `v2-native-${name}`, { sessionID: scope.sessionID, ...detail });
1786
+ }
1787
+ async function pauseActive(scope, reason, failed = true) {
1788
+ clear(scope, "deadline");
1789
+ const state = await read(scope);
1790
+ for (const job of state.jobs || []) {
1791
+ if (scope.active ? job.id !== scope.active.jobID : !eligible(job))
1792
+ continue;
1793
+ policy.pause(job, reason, failed);
1794
+ }
1795
+ await save(scope, state);
1796
+ await cancelPending(scope, state);
1797
+ await note(scope, "paused", { reason });
1798
+ return result({ reason });
1799
+ }
1800
+ async function cancelPending(scope, state) {
1801
+ const run = scope.active;
1802
+ if (!run || run.delivered || !["prompt", "command"].includes(run.kind) || !run.inboxID || typeof options.cancel !== "function")
1803
+ return false;
1804
+ let cancelled = false;
1805
+ try {
1806
+ cancelled = await options.cancel({ sessionID: scope.sessionID, inboxID: run.inboxID }) === true;
1807
+ } catch (error) {
1808
+ report(error);
1809
+ }
1810
+ if (!cancelled)
1811
+ return false;
1812
+ const job = (state.jobs || []).find((entry) => entry.id === run.jobID);
1813
+ if (job?.v2Run?.id === run.id) {
1814
+ Object.assign(job, run.previous);
1815
+ delete job.v2Run;
1816
+ await save(scope, state);
1817
+ }
1818
+ if (scope.active === run)
1819
+ delete scope.active;
1820
+ clear(scope, "deadline");
1821
+ return true;
1822
+ }
1823
+ async function finish(scope) {
1824
+ const run = scope.active;
1825
+ if (!run || scope.compaction || scope.busy)
1826
+ return result({ reason: "no-terminal-boundary" });
1827
+ if (run.kind === "prompt" && !run.delivered || run.kind === "command" && !run.commandFinished || run.kind === "shell" && !run.shellFinished || ["compact", "cadence"].includes(run.kind) && !run.compactFinished) {
1828
+ return result({ reason: "awaiting-owned-completion" });
1829
+ }
1830
+ clear(scope, "deadline");
1831
+ const state = await read(scope);
1832
+ const job = (state.jobs || []).find((entry) => entry.id === run.jobID);
1833
+ if (job?.v2Run?.id === run.id) {
1834
+ if (["compact", "cadence"].includes(run.kind)) {
1835
+ job.lastCompactAt = now();
1836
+ job.lastCompactRunCount = job.runCount || 0;
1837
+ } else if (!job.paused && !run.foreground) {
1838
+ await policy.finish(scope, job, () => current(scope) && scope.active === run && !run.foreground);
1839
+ }
1840
+ delete job.v2Run;
1841
+ job.lastCompletedAt = now();
1842
+ await save(scope, state);
1843
+ if (job.paused || job.enabled === false)
1844
+ await policy.notify(scope, job, job.pauseReason || "completed");
1845
+ }
1846
+ if (scope.active === run)
1847
+ delete scope.active;
1848
+ await note(scope, "completed", { jobID: run.jobID, kind: run.kind, foreground: Boolean(run.foreground) });
1849
+ return advance(scope);
1850
+ }
1851
+ async function dispatch(scope, state, job, kind) {
1852
+ const epoch = scope.epoch;
1853
+ const isCurrent = () => current(scope) && scope.epoch === epoch && !scope.busy && !scope.compaction;
1854
+ let request;
1855
+ if (kind === "prompt")
1856
+ request = { sessionID: scope.sessionID, id: id(), text: `${PREFIX}
1857
+
1858
+ ${await policy.buildPrompt(scope.directory, job)}`, delivery: "queue", metadata: { opencode_loop_v2: true, opencode_loop_job: job.id } };
1859
+ else if (kind === "compact" || kind === "cadence")
1860
+ request = { sessionID: scope.sessionID, id: id(), delivery: "queue" };
1861
+ else if (kind === "shell")
1862
+ request = { sessionID: scope.sessionID, id: id(), command: String(job.action).replace(/^[!$]\s*/, ""), timeoutMs: job.timeoutMs };
1863
+ else {
1864
+ const [name, text] = splitFirst(String(job.action).replace(/^\/+/, ""));
1865
+ request = { sessionID: scope.sessionID, name, text: text || "", delivery: "queue" };
1866
+ }
1867
+ if (!isCurrent())
1868
+ return result({ reason: "foreground-or-compaction" });
1869
+ if (job.dryRun) {
1870
+ policy.pause(job, "dry-run");
1871
+ await save(scope, state);
1872
+ return result({ dryRun: true, request });
1873
+ }
1874
+ if (kind === "shell" && job.safe) {
1875
+ await Promise.resolve().then(() => init_job_workspace());
1876
+ if (dangerousShell(request.command)) {
1877
+ policy.pause(job, "unsafe-shell", true);
1878
+ await save(scope, state);
1879
+ return result({ reason: "unsafe-shell" });
1880
+ }
1881
+ }
1882
+ const run = {
1883
+ id: request.id || id(),
1884
+ jobID: job.id,
1885
+ kind,
1886
+ createdAt: now(),
1887
+ delivered: false,
1888
+ previous: { runCount: job.runCount || 0, lastRunAt: job.lastRunAt || 0, enabled: job.enabled }
1889
+ };
1890
+ job.v2Run = { id: run.id, kind, status: "admitting", createdAt: run.createdAt };
1891
+ await save(scope, state);
1892
+ job = state.jobs.find((entry) => entry.id === run.jobID);
1893
+ if (!isCurrent() || !job || !eligible(job)) {
1894
+ if (job?.v2Run?.id === run.id) {
1895
+ delete job.v2Run;
1896
+ await save(scope, state);
1897
+ }
1898
+ return result({ reason: "admission-withdrawn" });
1899
+ }
1900
+ scope.active = run;
1901
+ clear(scope, "timer");
1902
+ try {
1903
+ let response;
1904
+ if (kind === "prompt")
1905
+ response = await options.prompt(request);
1906
+ else if (kind === "compact" || kind === "cadence")
1907
+ response = await options.compact(request);
1908
+ else if (kind === "shell")
1909
+ response = await options.shell(request);
1910
+ else
1911
+ response = await options.command(request);
1912
+ if (response?.error || response?.accepted === false)
1913
+ throw new Error(String(response.error?.message || response.error || "Native admission rejected"));
1914
+ run.inboxID = response?.id || response?.data?.id || request.id;
1915
+ run.shellID = kind === "shell" ? response?.id || response?.shell?.id || request.id : undefined;
1916
+ if (kind !== "cadence") {
1917
+ job.runCount = (job.runCount || 0) + 1;
1918
+ job.lastRunAt = now();
1919
+ if (job.maxRuns > 0 && job.runCount >= job.maxRuns)
1920
+ job.enabled = false;
1921
+ }
1922
+ delete job.runNowRequestedAt;
1923
+ job.watchTriggered = false;
1924
+ job.v2Run = { ...job.v2Run, status: "admitted", inboxID: run.inboxID, shellID: run.shellID };
1925
+ await save(scope, state);
1926
+ await note(scope, "admitted", { jobID: job.id, kind, inboxID: run.inboxID, runCount: job.runCount });
1927
+ if (current(scope) && job.timeoutMs > 0) {
1928
+ scope.deadline = setTimer(() => enqueue(scope, () => scope.active === run ? pauseActive(scope, "timeout-waiting-for-host-boundary") : result()).catch(report), Math.min(job.timeoutMs, 2147483647));
1929
+ scope.deadline?.unref?.();
1930
+ }
1931
+ if (kind === "command") {
1932
+ Promise.resolve().then(() => options.wait({ sessionID: scope.sessionID })).then(() => enqueue(scope, async () => {
1933
+ if (scope.active !== run)
1934
+ return result();
1935
+ run.commandFinished = true;
1936
+ return finish(scope);
1937
+ }), () => enqueue(scope, () => pauseActive(scope, "command-failed"))).catch(report);
1938
+ }
1939
+ if (kind === "shell" && response?.status && response.status !== "running") {
1940
+ run.shellFinished = true;
1941
+ if (response.status !== "exited" || Number(response.exit || 0) !== 0)
1942
+ return pauseActive(scope, "shell-failed");
1943
+ return finish(scope);
1944
+ }
1945
+ return result({ dispatched: true, job, kind, request });
1946
+ } catch (error) {
1947
+ policy.pause(job, "admission-uncertain", true);
1948
+ job.lastError = error instanceof Error ? error.message : String(error);
1949
+ await save(scope, state);
1950
+ await note(scope, "admission-failed", { jobID: job.id, message: job.lastError });
1951
+ return result({ reason: "admission-uncertain", error: job.lastError });
1952
+ }
1953
+ }
1954
+ async function advance(scope) {
1955
+ if (!current(scope) || scope.busy || scope.compaction || scope.active)
1956
+ return result({ reason: "host-not-idle" });
1957
+ const epoch = scope.epoch;
1958
+ const state = await read(scope);
1959
+ clear(scope, "companionTimer");
1960
+ if (!(state.jobs || []).some(eligible))
1961
+ return result();
1962
+ if (await nativeGoalReservesSession(scope.directory, scope.sessionID)) {
1963
+ if (current(scope)) {
1964
+ scope.companionTimer = setTimer(() => {
1965
+ delete scope.companionTimer;
1966
+ return enqueue(scope, () => advance(scope)).catch(report);
1967
+ }, 1000);
1968
+ scope.companionTimer?.unref?.();
1969
+ }
1970
+ return result({ reason: "dedicated-goal-owns-session" });
1971
+ }
1972
+ for (const job of state.jobs || []) {
1973
+ if (!eligible(job))
1974
+ continue;
1975
+ if (job.v2Run) {
1976
+ policy.pause(job, "restart-requires-review");
1977
+ await save(scope, state);
1978
+ return result({ reason: "restart-requires-review" });
1979
+ }
1980
+ if (blocked(job).length)
1981
+ continue;
1982
+ if (job.watchPaths?.length && await policy.watchChanged(scope.directory, job))
1983
+ job.watchTriggered = true;
1984
+ const safe = () => current(scope) && !scope.busy && !scope.compaction && !scope.active && scope.epoch === epoch;
1985
+ if (!safe())
1986
+ return result({ reason: "foreground-or-compaction" });
1987
+ if (!await policy.checkStop(scope, job)) {
1988
+ await save(scope, state);
1989
+ await policy.notify(scope, job, job.pauseReason);
1990
+ return advance(scope);
1991
+ }
1992
+ if (dueAt(job) > now())
1993
+ continue;
1994
+ if (!await policy.prepare(scope, job, safe)) {
1995
+ if (job.paused || job.enabled === false) {
1996
+ await save(scope, state);
1997
+ await policy.notify(scope, job, job.pauseReason);
1998
+ return advance(scope);
1999
+ }
2000
+ return result({ reason: job.pauseReason || "foreground-or-compaction" });
2001
+ }
2002
+ const kind = actionKind(job.action, job);
2003
+ const cadence = kind !== "compact" && job.runCount > 0 && (job.compactEveryRuns > 0 && job.runCount - (job.lastCompactRunCount || 0) >= job.compactEveryRuns || job.compactEveryMs > 0 && now() - (job.lastCompactAt || Date.parse(job.createdAt)) >= job.compactEveryMs);
2004
+ return dispatch(scope, state, job, cadence ? "cadence" : kind);
2005
+ }
2006
+ await schedule(scope);
2007
+ return result();
2008
+ }
2009
+ async function command(scope, event) {
2010
+ const state = await read(scope);
2011
+ const target = String(event.arguments || "").trim() || "all";
2012
+ if (event.name === "loop" || OPENCODE_LOOP_V2_PRESET_NAMES.includes(event.name)) {
2013
+ const defaults = event.name === "loop" ? {} : presetDefaults(event.name);
2014
+ const normalized = normalizeLoopScheduleArgs(event.arguments || "", defaults);
2015
+ if (!normalized.ok)
2016
+ return result({ accepted: false, error: normalized.error });
2017
+ if (normalized.allowGoalOverlap)
2018
+ return result({ accepted: false, error: "Native Goal session reservation cannot be overridden; use another session." });
2019
+ const parsed = parseLoopArgs(normalized.args, normalized.defaults);
2020
+ if (!parsed.ok)
2021
+ return result({ accepted: false, error: parsed.error });
2022
+ parsed.job.scheduleMode = normalized.scheduleMode;
2023
+ parsed.job.scheduleSyntax = normalized.scheduleSyntax;
2024
+ if (normalized.scheduleSyntax === "after") {
2025
+ parsed.job.immediate = false;
2026
+ parsed.job.maxRuns = 1;
2027
+ }
2028
+ applyTestfixPreset(parsed.job, defaults);
2029
+ const blockers = blocked(parsed.job);
2030
+ if (blockers.length)
2031
+ return result({ accepted: false, reason: "unsupported", blockers, error: blockers.join(" ") });
2032
+ const job = parsed.job;
2033
+ job.name = String(job.name || "default");
2034
+ job.createdAt = new Date(now()).toISOString();
2035
+ job.lastRunAt = 0;
2036
+ if (job.watchPaths?.length)
2037
+ job.watchSnapshot = await policy.snapshotPaths(scope.directory, job.watchPaths);
2038
+ const jobs = state.jobs || [];
2039
+ if (!job.multi && jobs.some((other) => (other.name || "default") === job.name && other.v2Run))
2040
+ return result({ accepted: false, error: "Pause/stop the existing in-flight job before replacing it." });
2041
+ state.jobs = job.multi ? jobs : jobs.filter((other) => (other.name || "default") !== job.name);
2042
+ state.jobs.push(job);
2043
+ await save(scope, state);
2044
+ await schedule(scope);
2045
+ return result({ accepted: true, job });
2046
+ }
2047
+ if (event.name === "loop-status" || event.name === "loop-export") {
2048
+ const status = formatOpenCode2LoopStatus(state, now());
2049
+ const text = event.name === "loop-export" ? JSON.stringify(state, null, 2) : status.text;
2050
+ await options.prompt({ sessionID: scope.sessionID, text, resume: false, metadata: { opencode_loop_v2: true } });
2051
+ return result({ accepted: true, status });
2052
+ }
2053
+ if (!["loop-now", "loop-pause", "loop-resume", "loop-stop", "loop-remove", "loop-clear"].includes(event.name))
2054
+ return { handled: false };
2055
+ const matches = (job, index) => event.name === "loop-clear" || matchJob(job, target, index);
2056
+ let count = 0;
2057
+ for (const [index, job] of (state.jobs || []).entries()) {
2058
+ if (!matches(job, index))
2059
+ continue;
2060
+ count++;
2061
+ if (["loop-pause", "loop-stop", "loop-remove", "loop-clear"].includes(event.name)) {
2062
+ policy.pause(job, "user-paused");
2063
+ } else if (!job.v2Run) {
2064
+ job.paused = false;
2065
+ delete job.pauseReason;
2066
+ if (event.name === "loop-now" || event.name === "loop-resume")
2067
+ job.runNowRequestedAt = Math.max(1, now());
2068
+ }
2069
+ }
2070
+ await save(scope, state);
2071
+ if (state.jobs.some((job) => job.id === scope.active?.jobID && job.paused))
2072
+ await cancelPending(scope, state);
2073
+ if (["loop-stop", "loop-remove", "loop-clear"].includes(event.name)) {
2074
+ state.jobs = state.jobs.filter((job, index) => !matches(job, index));
2075
+ if (!state.jobs.length)
2076
+ await removeState(scope.directory, scope.sessionID);
2077
+ else
2078
+ await save(scope, state);
2079
+ }
2080
+ await schedule(scope);
2081
+ return result({ accepted: true, count, target });
2082
+ }
2083
+ async function onEvent(event) {
2084
+ if (disposed)
2085
+ return result({ reason: "disposed" });
2086
+ if (event?.kind === "server" && event.action === "disposed") {
2087
+ await dispose();
2088
+ return result({ disposed: true });
2089
+ }
2090
+ const scope = scopeFor(event);
2091
+ if (!scope)
2092
+ return { handled: false };
2093
+ if (event.kind === "foreground") {
2094
+ scope.epoch++;
2095
+ scope.busy = true;
2096
+ if (scope.active)
2097
+ scope.active.foreground = true;
2098
+ clear(scope, "timer");
2099
+ return result({ reason: "foreground-admission" });
2100
+ }
2101
+ if (event.kind === "compaction" && event.action === "started") {
2102
+ scope.compaction ||= { ended: false, terminal: false };
2103
+ scope.busy = true;
2104
+ clear(scope, "timer");
2105
+ return result({ reason: "compacting" });
2106
+ }
2107
+ if (event.kind === "session" && event.action === "status" && ["busy", "retry"].includes(event.status)) {
2108
+ scope.busy = true;
2109
+ clear(scope, "timer");
2110
+ return result();
2111
+ }
2112
+ if (event.kind === "session" && event.action === "deleted") {
2113
+ scope.deleted = true;
2114
+ clear(scope, "timer");
2115
+ clear(scope, "deadline");
2116
+ clear(scope, "companionTimer");
2117
+ scopes.delete(scope.key);
2118
+ return result({ disposedScope: true });
2119
+ }
2120
+ return enqueue(scope, async () => {
2121
+ if (event.kind === "command" && event.action === "executed")
2122
+ return command(scope, event);
2123
+ if (event.kind === "inbox") {
2124
+ if (scope.active?.inboxID === event.inboxID || scope.active?.id === event.inboxID) {
2125
+ if (event.action === "delivered")
2126
+ scope.active.delivered = true;
2127
+ if (event.action === "cancelled") {
2128
+ const run = scope.active;
2129
+ const state = await read(scope);
2130
+ const job = (state.jobs || []).find((entry) => entry.id === run.jobID);
2131
+ if (job?.v2Run?.id === run.id && !run.delivered) {
2132
+ Object.assign(job, run.previous);
2133
+ policy.pause(job, "inbox-cancelled");
2134
+ delete job.v2Run;
2135
+ await save(scope, state);
2136
+ }
2137
+ if (scope.active === run)
2138
+ delete scope.active;
2139
+ clear(scope, "deadline");
2140
+ return result({ reason: "inbox-cancelled" });
2141
+ }
2142
+ }
2143
+ return result();
2144
+ }
2145
+ if (event.kind === "shell" && event.action === "ended" && scope.active?.shellID === event.shellID) {
2146
+ scope.active.shellFinished = true;
2147
+ if (event.status !== "exited" || event.code !== 0) {
2148
+ const run = scope.active;
2149
+ const paused = await pauseActive(scope, "shell-failed");
2150
+ const state = await read(scope);
2151
+ const job = (state.jobs || []).find((entry) => entry.id === run.jobID);
2152
+ if (job?.v2Run?.id === run.id) {
2153
+ delete job.v2Run;
2154
+ await save(scope, state);
2155
+ }
2156
+ if (scope.active === run)
2157
+ delete scope.active;
2158
+ return paused;
2159
+ }
2160
+ return finish(scope);
2161
+ }
2162
+ if (event.kind === "session" && ["error", "failed", "interrupted"].includes(event.action) || event.kind === "compaction" && event.action === "failed") {
2163
+ delete scope.compaction;
2164
+ scope.busy = false;
2165
+ const run = scope.active;
2166
+ const paused = await pauseActive(scope, event.reason || `${event.kind}-${event.action}`);
2167
+ if (run && (run.delivered || ["compact", "cadence", "shell"].includes(run.kind))) {
2168
+ const state = await read(scope);
2169
+ const job = (state.jobs || []).find((entry) => entry.id === run.jobID);
2170
+ if (job?.v2Run?.id === run.id) {
2171
+ delete job.v2Run;
2172
+ await save(scope, state);
2173
+ }
2174
+ if (scope.active === run)
2175
+ delete scope.active;
2176
+ }
2177
+ return paused;
2178
+ }
2179
+ if (event.kind === "compaction" && event.action === "ended") {
2180
+ scope.compaction ||= { ended: false, terminal: false };
2181
+ scope.compaction.ended = true;
2182
+ if (["compact", "cadence"].includes(scope.active?.kind))
2183
+ scope.active.compactFinished = true;
2184
+ if (!scope.compaction.terminal)
2185
+ return result({ reason: "awaiting-execution-terminal" });
2186
+ } else if (event.kind === "session" && event.action === "idle") {
2187
+ if (scope.compaction) {
2188
+ scope.compaction.terminal = true;
2189
+ if (!scope.compaction.ended)
2190
+ return result({ reason: "awaiting-compaction-end" });
2191
+ }
2192
+ } else if (event.kind === "session" && event.action === "status" && event.status === "idle") {
2193
+ if (scope.active || scope.compaction)
2194
+ return result({ reason: "awaiting-execution-terminal" });
2195
+ } else
2196
+ return { handled: false };
2197
+ delete scope.compaction;
2198
+ scope.busy = false;
2199
+ return scope.active ? finish(scope) : advance(scope);
2200
+ });
2201
+ }
2202
+ async function wake(event) {
2203
+ const scope = scopeFor(event);
2204
+ return scope ? enqueue(scope, () => advance(scope)) : { handled: false };
2205
+ }
2206
+ async function dispose() {
2207
+ if (disposed)
2208
+ return false;
2209
+ disposed = true;
2210
+ for (const scope of scopes.values()) {
2211
+ clear(scope, "timer");
2212
+ clear(scope, "deadline");
2213
+ clear(scope, "companionTimer");
2214
+ }
2215
+ await Promise.allSettled([...scopes.values()].map((scope) => scope.queue));
2216
+ scopes.clear();
2217
+ return true;
2218
+ }
2219
+ return Object.freeze({ onEvent, wake, dispose, scheduledCount: () => [...scopes.values()].filter((scope) => scope.timer !== undefined).length });
2220
+ }
2221
+
2222
+ // src/source/runtime/events.js
2223
+ function record(value) {
2224
+ return value && typeof value === "object" ? value : undefined;
2225
+ }
2226
+ function text(value) {
2227
+ return typeof value === "string" && value.trim() ? value : undefined;
2228
+ }
2229
+ function envelope(input) {
2230
+ const outer = record(input);
2231
+ if (!outer)
2232
+ return;
2233
+ const payload = record(outer.payload);
2234
+ if (payload && text(payload.type)) {
2235
+ return { directory: text(outer.directory), event: payload };
2236
+ }
2237
+ const wrappedEvent = record(outer.event);
2238
+ if (wrappedEvent && text(wrappedEvent.type)) {
2239
+ return { directory: text(outer.directory), event: wrappedEvent };
2240
+ }
2241
+ if (!text(outer.type))
2242
+ return;
2243
+ return { directory: undefined, event: outer };
2244
+ }
2245
+ function freezeEvent(value) {
2246
+ return Object.freeze(value);
2247
+ }
2248
+ function normalizeOpenCodeEvent(input) {
2249
+ const parsed = envelope(input);
2250
+ if (!parsed)
2251
+ return;
2252
+ const { directory, event } = parsed;
2253
+ const properties = record(event.properties) || {};
2254
+ if (event.type === "session.status") {
2255
+ const sessionID = text(properties.sessionID);
2256
+ const status = text(record(properties.status)?.type);
2257
+ if (!sessionID || !status)
2258
+ return;
2259
+ return freezeEvent({ kind: "session", action: "status", sessionID, directory, status });
2260
+ }
2261
+ if (event.type === "session.idle" || event.type === "session.compacted") {
2262
+ const sessionID = text(properties.sessionID);
2263
+ if (!sessionID)
2264
+ return;
2265
+ return freezeEvent({
2266
+ kind: "session",
2267
+ action: event.type === "session.idle" ? "idle" : "compacted",
2268
+ sessionID,
2269
+ directory
2270
+ });
2271
+ }
2272
+ if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.deleted") {
2273
+ const info = record(properties.info);
2274
+ const sessionID = text(info?.id);
2275
+ if (!sessionID)
2276
+ return;
2277
+ return freezeEvent({
2278
+ kind: "session",
2279
+ action: event.type.slice("session.".length),
2280
+ sessionID,
2281
+ directory: directory || text(info?.directory),
2282
+ parentID: text(info?.parentID)
2283
+ });
2284
+ }
2285
+ if (event.type === "session.error") {
2286
+ const sessionID = text(properties.sessionID);
2287
+ if (!sessionID)
2288
+ return;
2289
+ return freezeEvent({ kind: "session", action: "error", sessionID, directory });
2290
+ }
2291
+ if (event.type === "message.updated") {
2292
+ const info = record(properties.info);
2293
+ const sessionID = text(info?.sessionID);
2294
+ const messageID = text(info?.id);
2295
+ const role = text(info?.role);
2296
+ if (!sessionID || !messageID || !role)
2297
+ return;
2298
+ const time = record(info?.time);
2299
+ return freezeEvent({
2300
+ kind: "message",
2301
+ action: "updated",
2302
+ sessionID,
2303
+ directory,
2304
+ messageID,
2305
+ role,
2306
+ completedAt: Number.isFinite(time?.completed) ? time.completed : undefined,
2307
+ finish: text(info?.finish)
2308
+ });
2309
+ }
2310
+ if (event.type === "command.executed") {
2311
+ const sessionID = text(properties.sessionID);
2312
+ const name = text(properties.name);
2313
+ if (!sessionID || !name)
2314
+ return;
2315
+ return freezeEvent({
2316
+ kind: "command",
2317
+ action: "executed",
2318
+ sessionID,
2319
+ directory,
2320
+ name,
2321
+ arguments: typeof properties.arguments === "string" ? properties.arguments : "",
2322
+ messageID: text(properties.messageID)
2323
+ });
2324
+ }
2325
+ if (event.type === "server.instance.disposed") {
2326
+ const disposedDirectory = directory || text(properties.directory);
2327
+ if (!disposedDirectory)
2328
+ return;
2329
+ return freezeEvent({ kind: "server", action: "disposed", directory: disposedDirectory });
2330
+ }
2331
+ return;
2332
+ }
2333
+
2334
+ // src/source/runtime/session-registry.js
2335
+ var DEFAULT_SESSION_STALE_MS = 12 * 60 * 60 * 1000;
2336
+ function sessionKey(sessionID) {
2337
+ return String(sessionID || "").trim();
2338
+ }
2339
+ function createSessionRegistry({ now = Date.now, staleAfterMs = DEFAULT_SESSION_STALE_MS } = {}) {
2340
+ if (typeof now !== "function")
2341
+ throw new TypeError("session registry requires a clock function");
2342
+ if (!Number.isFinite(staleAfterMs) || staleAfterMs < 0)
2343
+ throw new TypeError("session registry requires a non-negative staleAfterMs");
2344
+ const sessions = new Map;
2345
+ function observeExternal(sessionID, runtime) {
2346
+ const key = sessionKey(sessionID);
2347
+ if (!key)
2348
+ throw new TypeError("session registry requires a session ID");
2349
+ const seenAt = Number(now());
2350
+ if (!Number.isFinite(seenAt))
2351
+ throw new TypeError("session registry clock must return a finite number");
2352
+ const entry = Object.freeze({ sessionID: key, runtime, seenAt });
2353
+ sessions.set(key, entry);
2354
+ return entry;
2355
+ }
2356
+ function peek(sessionID) {
2357
+ return sessions.get(sessionKey(sessionID));
2358
+ }
2359
+ function remove(sessionID, expectedRuntime) {
2360
+ const key = sessionKey(sessionID);
2361
+ const current = sessions.get(key);
2362
+ if (!current)
2363
+ return false;
2364
+ if (arguments.length > 1 && current.runtime !== expectedRuntime)
2365
+ return false;
2366
+ return sessions.delete(key);
2367
+ }
2368
+ function pruneStale(at = now()) {
2369
+ const timestamp = Number(at);
2370
+ if (!Number.isFinite(timestamp))
2371
+ throw new TypeError("session registry prune time must be finite");
2372
+ const removed = [];
2373
+ for (const [key, entry] of sessions) {
2374
+ if (timestamp - entry.seenAt < staleAfterMs)
2375
+ continue;
2376
+ sessions.delete(key);
2377
+ removed.push(key);
2378
+ }
2379
+ return removed;
2380
+ }
2381
+ function entries() {
2382
+ return [...sessions.values()];
2383
+ }
2384
+ return Object.freeze({
2385
+ observeExternal,
2386
+ peek,
2387
+ remove,
2388
+ pruneStale,
2389
+ entries
2390
+ });
2391
+ }
2392
+
2393
+ // src/source/runtime/scope.js
2394
+ function createRuntimeScope() {
2395
+ const controller = new AbortController;
2396
+ const cleanups = new Set;
2397
+ let disposed = false;
2398
+ function track(cleanup) {
2399
+ if (typeof cleanup !== "function")
2400
+ throw new TypeError("runtime scope cleanup must be a function");
2401
+ if (disposed) {
2402
+ cleanup();
2403
+ return () => false;
2404
+ }
2405
+ const entry = { cleanup };
2406
+ cleanups.add(entry);
2407
+ let tracked = true;
2408
+ return () => {
2409
+ if (!tracked)
2410
+ return false;
2411
+ tracked = false;
2412
+ return cleanups.delete(entry);
2413
+ };
2414
+ }
2415
+ function guard(callback) {
2416
+ if (typeof callback !== "function")
2417
+ throw new TypeError("runtime scope guard requires a function");
2418
+ return function(...args) {
2419
+ if (disposed)
2420
+ return;
2421
+ return callback.apply(this, args);
2422
+ };
2423
+ }
2424
+ function dispose(reason) {
2425
+ if (disposed)
2426
+ return false;
2427
+ disposed = true;
2428
+ controller.abort(reason);
2429
+ const errors = [];
2430
+ for (const entry of [...cleanups].reverse()) {
2431
+ cleanups.delete(entry);
2432
+ try {
2433
+ entry.cleanup();
2434
+ } catch (error) {
2435
+ errors.push(error);
2436
+ }
2437
+ }
2438
+ if (errors.length)
2439
+ throw new AggregateError(errors, "runtime scope cleanup failed");
2440
+ return true;
2441
+ }
2442
+ return Object.freeze({
2443
+ signal: controller.signal,
2444
+ isActive: () => !disposed,
2445
+ track,
2446
+ guard,
2447
+ dispose
2448
+ });
2449
+ }
2450
+
2451
+ // src/source/runtime/timers.js
2452
+ function createOwnedTimer(scope, callback, delay, repeat, api, ref) {
2453
+ if (!scope?.isActive?.())
2454
+ return;
2455
+ let active = true;
2456
+ let release;
2457
+ let handle;
2458
+ const invoke = scope.guard((...args) => {
2459
+ if (!active)
2460
+ return;
2461
+ if (!repeat) {
2462
+ active = false;
2463
+ release?.();
2464
+ }
2465
+ callback(...args);
2466
+ });
2467
+ handle = repeat ? api.setInterval(invoke, delay) : api.setTimeout(invoke, delay);
2468
+ release = scope.track(() => {
2469
+ if (repeat)
2470
+ api.clearInterval(handle);
2471
+ else
2472
+ api.clearTimeout(handle);
2473
+ });
2474
+ if (!ref)
2475
+ handle?.unref?.();
2476
+ return Object.freeze({
2477
+ handle,
2478
+ cancel() {
2479
+ if (!active)
2480
+ return false;
2481
+ active = false;
2482
+ if (repeat)
2483
+ api.clearInterval(handle);
2484
+ else
2485
+ api.clearTimeout(handle);
2486
+ release();
2487
+ return true;
2488
+ }
2489
+ });
2490
+ }
2491
+ function createRuntimeTimers(scope, api = globalThis) {
2492
+ return Object.freeze({
2493
+ timeout(callback, delay, options = {}) {
2494
+ return createOwnedTimer(scope, callback, delay, false, api, options.ref !== false);
2495
+ },
2496
+ interval(callback, delay, options = {}) {
2497
+ return createOwnedTimer(scope, callback, delay, true, api, options.ref !== false);
2498
+ }
2499
+ });
2500
+ }
2501
+
2502
+ // src/source/runtime/session-manager.js
2503
+ function defaultRuntimeFactory({ sessionID, timerAPI }) {
2504
+ const scope = createRuntimeScope();
2505
+ return Object.freeze({
2506
+ sessionID,
2507
+ scope,
2508
+ timers: createRuntimeTimers(scope, timerAPI),
2509
+ dispose: (reason) => scope.dispose(reason)
2510
+ });
2511
+ }
2512
+ function validateRuntime(runtime, sessionID) {
2513
+ if (!runtime || runtime.sessionID !== sessionID)
2514
+ throw new TypeError("session runtime factory must preserve the session ID");
2515
+ if (typeof runtime?.scope?.isActive !== "function")
2516
+ throw new TypeError("session runtime factory must provide an active scope");
2517
+ if (typeof runtime.dispose !== "function")
2518
+ throw new TypeError("session runtime factory must provide dispose()");
2519
+ return runtime;
2520
+ }
2521
+ function createSessionRuntimeManager({
2522
+ now = Date.now,
2523
+ staleAfterMs = DEFAULT_SESSION_STALE_MS,
2524
+ timerAPI = globalThis,
2525
+ runtimeFactory = defaultRuntimeFactory
2526
+ } = {}) {
2527
+ if (typeof runtimeFactory !== "function")
2528
+ throw new TypeError("session runtime manager requires a runtime factory");
2529
+ const registry = createSessionRegistry({ now, staleAfterMs });
2530
+ let disposed = false;
2531
+ function observeExternal(sessionID) {
2532
+ if (disposed)
2533
+ throw new Error("session runtime manager is disposed");
2534
+ const key = String(sessionID || "").trim();
2535
+ if (!key)
2536
+ throw new TypeError("session runtime manager requires a session ID");
2537
+ const current = registry.peek(key);
2538
+ const runtime = current?.runtime?.scope?.isActive?.() ? current.runtime : validateRuntime(runtimeFactory({ sessionID: key, timerAPI }), key);
2539
+ registry.observeExternal(key, runtime);
2540
+ return runtime;
2541
+ }
2542
+ function peek(sessionID) {
2543
+ return registry.peek(sessionID)?.runtime;
2544
+ }
2545
+ function entries() {
2546
+ return registry.entries();
2547
+ }
2548
+ function remove(sessionID, { expectedRuntime, reason } = {}) {
2549
+ const current = registry.peek(sessionID);
2550
+ if (!current)
2551
+ return false;
2552
+ if (expectedRuntime !== undefined && current.runtime !== expectedRuntime)
2553
+ return false;
2554
+ if (!registry.remove(sessionID, current.runtime))
2555
+ return false;
2556
+ current.runtime.dispose(reason);
2557
+ return true;
2558
+ }
2559
+ function pruneStale(at = now()) {
2560
+ const before = new Map(registry.entries().map((entry) => [entry.sessionID, entry.runtime]));
2561
+ const removed = registry.pruneStale(at);
2562
+ const errors = [];
2563
+ for (const sessionID of removed) {
2564
+ try {
2565
+ before.get(sessionID)?.dispose("stale-session");
2566
+ } catch (error) {
2567
+ errors.push(error);
2568
+ }
2569
+ }
2570
+ if (errors.length)
2571
+ throw new AggregateError(errors, "stale session cleanup failed");
2572
+ return removed;
2573
+ }
2574
+ function dispose(reason) {
2575
+ if (disposed)
2576
+ return false;
2577
+ disposed = true;
2578
+ const errors = [];
2579
+ for (const entry of registry.entries()) {
2580
+ registry.remove(entry.sessionID, entry.runtime);
2581
+ try {
2582
+ entry.runtime.dispose(reason);
2583
+ } catch (error) {
2584
+ errors.push(error);
2585
+ }
2586
+ }
2587
+ if (errors.length)
2588
+ throw new AggregateError(errors, "session runtime manager cleanup failed");
2589
+ return true;
2590
+ }
2591
+ return Object.freeze({
2592
+ observeExternal,
2593
+ peek,
2594
+ entries,
2595
+ remove,
2596
+ pruneStale,
2597
+ dispose
2598
+ });
2599
+ }
2600
+
2601
+ // src/source/opencode2/events.js
2602
+ function record2(value) {
2603
+ return value && typeof value === "object" ? value : undefined;
2604
+ }
2605
+ function text2(value) {
2606
+ return typeof value === "string" && value.trim() ? value : undefined;
2607
+ }
2608
+ function directoryFrom(raw) {
2609
+ return text2(record2(raw?.location)?.directory);
2610
+ }
2611
+ function sessionIDFrom(raw) {
2612
+ const data = record2(raw?.data);
2613
+ return text2(data?.sessionID);
2614
+ }
2615
+ function normalizeOpenCode2NativeEvent(raw) {
2616
+ const type = text2(raw?.type);
2617
+ if (!type)
2618
+ return;
2619
+ const data = record2(raw?.data) || {};
2620
+ const directory = directoryFrom(raw);
2621
+ const sessionID = sessionIDFrom(raw);
2622
+ if (type === "session.inbox.enqueued") {
2623
+ const item = record2(data.item);
2624
+ const payload = record2(item?.payload);
2625
+ const parsed = item?.type === "user" ? parseOpenCode2LoopCommandText(payload?.text) : undefined;
2626
+ if (!sessionID || !parsed)
2627
+ return;
2628
+ return Object.freeze({
2629
+ kind: "command",
2630
+ action: "executed",
2631
+ sessionID,
2632
+ directory,
2633
+ name: parsed.name,
2634
+ arguments: parsed.arguments
2635
+ });
2636
+ }
2637
+ if (type === "session.status") {
2638
+ const status = text2(record2(data.status)?.type);
2639
+ if (!sessionID || !status)
2640
+ return;
2641
+ return Object.freeze({ kind: "session", action: "status", sessionID, directory, status });
2642
+ }
2643
+ if (type === "session.idle") {
2644
+ if (!sessionID)
2645
+ return;
2646
+ return Object.freeze({ kind: "session", action: "idle", sessionID, directory });
2647
+ }
2648
+ if (type === "session.execution.started") {
2649
+ if (!sessionID)
2650
+ return;
2651
+ return Object.freeze({ kind: "session", action: "status", sessionID, directory, status: "busy" });
2652
+ }
2653
+ if (type === "session.execution.succeeded") {
2654
+ if (!sessionID)
2655
+ return;
2656
+ return Object.freeze({ kind: "session", action: "idle", sessionID, directory });
2657
+ }
2658
+ if (type === "session.inbox.delivered" || type === "session.inbox.cancelled") {
2659
+ if (!sessionID || !text2(data.inboxID))
2660
+ return;
2661
+ return Object.freeze({ kind: "inbox", action: type.endsWith("delivered") ? "delivered" : "cancelled", sessionID, directory, inboxID: data.inboxID });
2662
+ }
2663
+ if (["session.compaction.started", "session.compaction.ended", "session.compaction.failed"].includes(type)) {
2664
+ if (!sessionID)
2665
+ return;
2666
+ return Object.freeze({ kind: "compaction", action: type.split(".").at(-1), sessionID, directory });
2667
+ }
2668
+ if (["session.execution.failed", "session.execution.interrupted"].includes(type)) {
2669
+ if (!sessionID)
2670
+ return;
2671
+ return Object.freeze({ kind: "session", action: "error", sessionID, directory, reason: type });
2672
+ }
2673
+ if (["session.shell.started", "session.shell.ended"].includes(type)) {
2674
+ if (!sessionID)
2675
+ return;
2676
+ const shell = record2(data.shell) || {};
2677
+ return Object.freeze({ kind: "shell", action: type.endsWith("started") ? "started" : "ended", sessionID, directory, shellID: shell.id, command: shell.command, code: typeof shell.exit === "number" ? shell.exit : -1, status: shell.status, metadata: shell.metadata });
2678
+ }
2679
+ if (type === "location.shutdown")
2680
+ return Object.freeze({ kind: "server", action: "disposed", directory });
2681
+ if (type === "session.created") {
2682
+ if (!sessionID)
2683
+ return;
2684
+ return Object.freeze({ kind: "session", action: "created", sessionID, directory: directory || text2(record2(data.location)?.directory) });
2685
+ }
2686
+ if (type === "session.deleted") {
2687
+ if (!sessionID)
2688
+ return;
2689
+ return Object.freeze({ kind: "session", action: "deleted", sessionID, directory });
2690
+ }
2691
+ return;
2692
+ }
2693
+
2694
+ // src/source/opencode2/event-bridge.js
2695
+ function cleanupRegistration(registration) {
2696
+ if (typeof registration === "function")
2697
+ return registration;
2698
+ if (!registration || typeof registration !== "object")
2699
+ return;
2700
+ for (const key of ["dispose", "unsubscribe", "close"]) {
2701
+ if (typeof registration[key] === "function")
2702
+ return registration[key].bind(registration);
2703
+ }
2704
+ return;
2705
+ }
2706
+ function eventStream(registration) {
2707
+ const stream = registration?.stream ?? registration;
2708
+ if (!stream || typeof stream[Symbol.asyncIterator] !== "function") {
2709
+ throw new TypeError("OpenCode 2 event subscribe must return an async event stream");
2710
+ }
2711
+ return stream;
2712
+ }
2713
+ function sameDirectory(expected, actual) {
2714
+ if (!expected || !actual)
2715
+ return true;
2716
+ return String(expected) === String(actual);
2717
+ }
2718
+ function createOpenCode2EventBridge({
2719
+ directory,
2720
+ allowInboxCommands = true,
2721
+ onEvent = async () => {},
2722
+ onError = () => {},
2723
+ runtimeManager = createSessionRuntimeManager()
2724
+ } = {}) {
2725
+ if (typeof onEvent !== "function")
2726
+ throw new TypeError("OpenCode 2 event bridge requires onEvent to be a function");
2727
+ if (typeof onError !== "function")
2728
+ throw new TypeError("OpenCode 2 event bridge requires onError to be a function");
2729
+ let registration;
2730
+ let iterator;
2731
+ let iteratorClosed = false;
2732
+ let pump;
2733
+ let attached = false;
2734
+ let stopped = false;
2735
+ let disposed = false;
2736
+ let managerDisposed = false;
2737
+ let queue = Promise.resolve();
2738
+ const sessionDirectories = new Map;
2739
+ const seenEventIDs = new Set;
2740
+ function report(error) {
2741
+ try {
2742
+ onError(error);
2743
+ } catch {}
2744
+ }
2745
+ function disposeManager(reason) {
2746
+ if (managerDisposed)
2747
+ return false;
2748
+ managerDisposed = true;
2749
+ return runtimeManager.dispose(reason);
2750
+ }
2751
+ async function closeIterator() {
2752
+ if (iteratorClosed)
2753
+ return false;
2754
+ iteratorClosed = true;
2755
+ const current = iterator;
2756
+ iterator = undefined;
2757
+ if (typeof current?.return === "function")
2758
+ await current.return();
2759
+ return Boolean(current);
2760
+ }
2761
+ function normalize(raw) {
2762
+ const current = normalizeOpenCode2NativeEvent(raw) || normalizeOpenCodeEvent(raw);
2763
+ if (!current)
2764
+ return;
2765
+ const sessionID = current.sessionID;
2766
+ const rememberedDirectory = sessionID ? sessionDirectories.get(sessionID) : undefined;
2767
+ const eventDirectory = current.directory || rememberedDirectory || directory;
2768
+ const event = eventDirectory === current.directory ? current : Object.freeze({ ...current, directory: eventDirectory });
2769
+ if (sessionID && event.directory)
2770
+ sessionDirectories.set(sessionID, event.directory);
2771
+ return event;
2772
+ }
2773
+ async function process2(raw) {
2774
+ if (stopped)
2775
+ return;
2776
+ if (!allowInboxCommands && raw?.type === "session.inbox.enqueued")
2777
+ return;
2778
+ const event = normalize(raw);
2779
+ if (!event || !sameDirectory(directory, event.directory))
2780
+ return;
2781
+ if (typeof raw?.id === "string") {
2782
+ if (seenEventIDs.has(raw.id))
2783
+ return;
2784
+ seenEventIDs.add(raw.id);
2785
+ if (seenEventIDs.size > 2048)
2786
+ seenEventIDs.delete(seenEventIDs.values().next().value);
2787
+ }
2788
+ const runtime = event.sessionID ? runtimeManager.observeExternal(event.sessionID) : undefined;
2789
+ await onEvent(event, runtime);
2790
+ if (event.kind === "session" && event.action === "deleted") {
2791
+ sessionDirectories.delete(event.sessionID);
2792
+ runtimeManager.remove(event.sessionID, { expectedRuntime: runtime, reason: "session-deleted" });
2793
+ }
2794
+ if (event.kind === "server" && event.action === "disposed") {
2795
+ stopped = true;
2796
+ sessionDirectories.clear();
2797
+ disposeManager("server-disposed");
2798
+ }
2799
+ return event;
2800
+ }
2801
+ function dispatch(raw) {
2802
+ if (stopped)
2803
+ return Promise.resolve(undefined);
2804
+ const result = queue.then(() => process2(raw));
2805
+ queue = result.catch(() => {
2806
+ return;
2807
+ });
2808
+ return result;
2809
+ }
2810
+ function callback(raw) {
2811
+ const pending = dispatch(raw);
2812
+ pending.catch(report);
2813
+ return pending.catch(() => {
2814
+ return;
2815
+ });
2816
+ }
2817
+ async function consume(stream) {
2818
+ const current = stream[Symbol.asyncIterator]();
2819
+ iterator = current;
2820
+ iteratorClosed = false;
2821
+ try {
2822
+ while (!stopped) {
2823
+ const next = await current.next();
2824
+ if (next?.done)
2825
+ break;
2826
+ await dispatch(next?.value);
2827
+ }
2828
+ } catch (error) {
2829
+ if (!stopped)
2830
+ report(error);
2831
+ } finally {
2832
+ if (stopped && !iteratorClosed)
2833
+ await closeIterator().catch(() => {
2834
+ return;
2835
+ });
2836
+ if (iterator === current)
2837
+ iterator = undefined;
2838
+ }
2839
+ }
2840
+ async function attach(subscribe) {
2841
+ if (disposed)
2842
+ throw new Error("OpenCode 2 event bridge is disposed");
2843
+ if (attached)
2844
+ throw new Error("OpenCode 2 event bridge is already attached");
2845
+ if (typeof subscribe !== "function")
2846
+ throw new TypeError("OpenCode 2 event bridge requires an event subscribe function");
2847
+ if (subscribe.length > 0) {
2848
+ registration = await subscribe(callback);
2849
+ attached = true;
2850
+ return registration;
2851
+ }
2852
+ registration = await subscribe();
2853
+ const stream = eventStream(registration);
2854
+ attached = true;
2855
+ pump = consume(stream);
2856
+ return registration;
2857
+ }
2858
+ async function dispose(reason = "bridge-disposed") {
2859
+ if (disposed)
2860
+ return false;
2861
+ disposed = true;
2862
+ stopped = true;
2863
+ await closeIterator().catch(() => {
2864
+ return;
2865
+ });
2866
+ await pump?.catch(() => {
2867
+ return;
2868
+ });
2869
+ await queue.catch(() => {
2870
+ return;
2871
+ });
2872
+ const cleanup = cleanupRegistration(registration);
2873
+ registration = undefined;
2874
+ try {
2875
+ if (cleanup)
2876
+ await cleanup();
2877
+ } finally {
2878
+ sessionDirectories.clear();
2879
+ disposeManager(reason);
2880
+ }
2881
+ return true;
2882
+ }
2883
+ return Object.freeze({
2884
+ attach,
2885
+ dispatch,
2886
+ dispose,
2887
+ runtimeManager,
2888
+ isAttached: () => attached,
2889
+ isDisposed: () => disposed
2890
+ });
2891
+ }
2892
+
2893
+ // src/source/opencode2/native-plugin.js
2894
+ init_process();
2895
+ init_state();
2896
+
2897
+ // src/source/core/progress.js
2898
+ import { promises as fs4 } from "fs";
2899
+ import path8 from "path";
2900
+ var DEFAULT_PROGRESS_MD = `# Progress
2901
+
2902
+ ## Current Goal
2903
+ Describe the current project goal here.
2904
+
2905
+ ## Agent Rules
2906
+ - Do not ask questions unless truly blocked.
2907
+ - Make reasonable assumptions and continue.
2908
+ - Work on unfinished TODOs in order.
2909
+ - Mark completed TODOs with [x].
2910
+ - Add new bugs, ideas, and follow-up work as TODOs.
2911
+ - Run tests, lint, or build when available.
2912
+ - Do not run destructive commands, force pushes, production deploys, or database resets.
2913
+
2914
+ ## Active TODO
2915
+ - [ ] Review the project structure and pick the next safe improvement.
2916
+
2917
+ ## Completed
2918
+ - [x] Created progress.md.
2919
+
2920
+ ## Backlog Ideas
2921
+ - [ ] Add more project-specific tasks here.
2922
+
2923
+ ## Blocked
2924
+ - None.
2925
+ `;
2926
+ function inside(root, candidate) {
2927
+ const relative = path8.relative(root, candidate);
2928
+ return relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
2929
+ }
2930
+ async function initializeProgressFile(directory, argumentsText = "") {
2931
+ const root = await fs4.realpath(directory);
2932
+ const target = String(argumentsText || "").trim() || "progress.md";
2933
+ const file = path8.resolve(root, target);
2934
+ const relative = path8.relative(root, file);
2935
+ if (!relative || !inside(root, file))
2936
+ throw new Error("Progress file must be inside the project");
2937
+ if ([".git", ".opencode"].includes(relative.split(path8.sep)[0].toLowerCase())) {
2938
+ throw new Error("Progress file cannot replace a control-plane path");
2939
+ }
2940
+ const parent = await fs4.realpath(path8.dirname(file));
2941
+ if (!inside(root, parent))
2942
+ throw new Error("Progress file must remain inside the project; symlink escape refused");
2943
+ try {
2944
+ await fs4.writeFile(file, DEFAULT_PROGRESS_MD, { encoding: "utf8", flag: "wx" });
2945
+ return { created: true, file: relative };
2946
+ } catch (error) {
2947
+ if (error?.code === "EEXIST")
2948
+ return { created: false, file: relative };
2949
+ throw error;
2950
+ }
2951
+ }
2952
+
2953
+ // src/source/opencode2/native-plugin.js
2954
+ var HELP = `OpenCode Loop: native OpenCode 2 runtime
2955
+ /loop 0s <task> --max-runs 3
2956
+ /loop 5m <task> --verify "npm test" --pause-on-verify-fail
2957
+ /loop --watch progress.md <task>
2958
+ /loop 0s <task> --prompt-file instructions.md --include-file progress.md
2959
+ /loop 0s <task> --compact-every 3
2960
+ /loop 5m --shell npm test
2961
+ /loop 15m --compact /compact
2962
+ Presets: /loop-dev, /loop-testfix, /loop-progress, /loop-safe-dev, /loop-ask, /loop-prompt, /loop-command, /loop-cmd, /loop-shell, /loop-compact.
2963
+ /loop-init [progress.md] creates a missing project-local progress file without overwriting existing data.
2964
+ Controls: /loop-now, /loop-pause, /loop-resume, /loop-stop, /loop-remove, /loop-clear, /loop-status, /loop-export.
2965
+ Preflight, postrun, notifications, stop files, completion markers, runtime/failure/run limits, branches and checkpoints are supported.
2966
+ Scheduled shell commands run as bounded local child processes when the plugin context has no session.shell.
2967
+ Scheduled/manual compaction requires session.compact on the host; unavailable capability is rejected before creating a job. Native automatic compaction is always left to OpenCode.
2968
+ An unfinished dedicated Goal reserves its session; Loop will not override it.
2969
+ --timeout pauses future iterations without aborting the current native model/tool/compaction operation.
2970
+ --safe is a command heuristic, not a sandbox. --dry-run does not dispatch or run hooks.
2971
+ Native sessions stay serialized; --allow-overlap is rejected.
2972
+ Use @bybrawe/opencode-goal and /goal for native Goal contracts. Legacy Loop Goal state is not silently migrated.
2973
+ An uncertain admission after a restart is paused for review instead of being replayed automatically.`;
2974
+ async function cleanup(value) {
2975
+ if (typeof value === "function")
2976
+ await value();
2977
+ else if (typeof value?.dispose === "function")
2978
+ await value.dispose();
2979
+ else if (typeof value?.unsubscribe === "function")
2980
+ await value.unsubscribe();
2981
+ }
2982
+ var OpenCodeLoopNativePlugin = Object.freeze({
2983
+ id: "@bybrawe/opencode-loop",
2984
+ async setup(ctx) {
2985
+ for (const [label, fn] of [["session.prompt", ctx?.session?.prompt], ["session.hook", ctx?.session?.hook], ["event.subscribe", ctx?.event?.subscribe], ["command.transform", ctx?.command?.transform]]) {
2986
+ if (typeof fn !== "function")
2987
+ throw new Error(`Native OpenCode Loop requires ${label}; use OpenCode 2.0.18 or newer.`);
2988
+ }
2989
+ const directory = String(ctx.location?.directory || ctx.options?.directory || "").trim();
2990
+ if (!directory)
2991
+ throw new Error("Native OpenCode Loop requires a project directory");
2992
+ const ownsSession = createNativeSessionLocationGuard({
2993
+ directory,
2994
+ workspaceID: ctx.location?.workspaceID,
2995
+ getSession: (input) => ctx.session.get(input)
2996
+ });
2997
+ const registrations = [];
2998
+ const lifecycleAbort = new AbortController;
2999
+ const prompt = (request) => ctx.session.prompt({ ...request, delivery: request.delivery || "queue", metadata: { ...request.metadata, opencode_loop_v2: true } });
3000
+ const shellHost = createNativeShellHost({
3001
+ directory,
3002
+ onTerminal: (event) => runtime.onEvent(event),
3003
+ onError: (error) => {
3004
+ appendLoopLog(directory, "v2-shell-error", { message: String(error?.message || error) }).catch(() => {});
3005
+ }
3006
+ });
3007
+ const runtime = createNativeLoopRuntime({
3008
+ directory,
3009
+ scopeAllowed: (scope) => ownsSession(scope.sessionID),
3010
+ prompt,
3011
+ command: typeof ctx.session.command === "function" ? (request) => ctx.session.command(request) : undefined,
3012
+ wait: typeof ctx.session.wait === "function" ? (request) => ctx.session.wait(request) : undefined,
3013
+ compact: typeof ctx.session.compact === "function" ? (request) => ctx.session.compact({ ...request, delivery: "queue" }) : undefined,
3014
+ shell: typeof ctx.session.shell === "function" ? (request) => ctx.session.shell(request) : (request) => shellHost.dispatch(request),
3015
+ cancel: typeof ctx.session.inbox?.cancel === "function" ? async (request) => {
3016
+ await ctx.session.inbox.cancel(request);
3017
+ return false;
3018
+ } : undefined,
3019
+ onError: (error) => {
3020
+ appendLoopLog(directory, "v2-native-error", { message: String(error?.message || error) }).catch(() => {});
3021
+ }
3022
+ });
3023
+ const bridge = createOpenCode2EventBridge({ directory, allowInboxCommands: false, onEvent: async (event) => {
3024
+ if (event.sessionID && !(event.kind === "session" && event.action === "deleted") && !await ownsSession(event.sessionID))
3025
+ return;
3026
+ return runtime.onEvent(event);
3027
+ }, onError: (error) => {
3028
+ appendLoopLog(directory, "v2-native-event-error", { message: String(error?.message || error) }).catch(() => {});
3029
+ } });
3030
+ let closed = false;
3031
+ let disposeTask;
3032
+ function dispose() {
3033
+ if (disposeTask)
3034
+ return disposeTask;
3035
+ closed = true;
3036
+ disposeTask = Promise.resolve().then(async () => {
3037
+ const errors = [];
3038
+ const stopped = bridge.dispose("native-plugin-disposed").catch((error) => {
3039
+ errors.push(error);
3040
+ });
3041
+ lifecycleAbort.abort();
3042
+ for (const action of [() => runtime.dispose(), () => shellHost.dispose()]) {
3043
+ try {
3044
+ await action();
3045
+ } catch (error) {
3046
+ errors.push(error);
3047
+ }
3048
+ }
3049
+ await stopped;
3050
+ for (const registration of [...registrations].reverse()) {
3051
+ try {
3052
+ await cleanup(registration);
3053
+ } catch (error) {
3054
+ errors.push(error);
3055
+ }
3056
+ }
3057
+ if (errors.length)
3058
+ throw new AggregateError(errors, "Native Loop cleanup failed");
3059
+ });
3060
+ return disposeTask;
3061
+ }
3062
+ async function execute({ name, sessionID, arguments: argumentsText }) {
3063
+ if (closed)
3064
+ throw new Error("Native Loop is disposed");
3065
+ if (!await ownsSession(sessionID))
3066
+ throw new Error("Native Loop cannot resolve this session in the plugin location; use its own project/worktree.");
3067
+ if (closed)
3068
+ throw new Error("Native Loop is disposed");
3069
+ if (name === "loop-init") {
3070
+ const output = await initializeProgressFile(directory, argumentsText);
3071
+ await prompt({ sessionID, text: output.created ? `Created ${output.file}.` : `${output.file} already exists; preserved unchanged.`, resume: false });
3072
+ return { handled: true, accepted: true, ...output };
3073
+ }
3074
+ if (["loop-help", "loop-doctor", "loop-logs"].includes(name)) {
3075
+ const text = name === "loop-logs" ? (await readSmallTextFile(path9.join(stateDir(directory), "loop.log"), 2000000)).split(`
3076
+ `).slice(-60).join(`
3077
+ `) || "No Loop log entries." : name === "loop-doctor" ? `Native OpenCode Loop
3078
+ Host: ${ctx.app?.version || "unknown"}
3079
+ Directory: ${directory}
3080
+ Prompt hooks: enabled
3081
+ Compaction ownership: native host
3082
+ Delivery: durable queue
3083
+ Scheduled timers: ${runtime.scheduledCount()}
3084
+ Use /loop-status for paused/admitted job state.` : HELP;
3085
+ await prompt({ sessionID, text, resume: false });
3086
+ return { handled: true, accepted: true };
3087
+ }
3088
+ const event = { kind: "command", action: "executed", directory, sessionID, name, arguments: argumentsText };
3089
+ const result = await runtime.onEvent(event);
3090
+ if (result?.accepted === false)
3091
+ throw new Error(result.error || result.blockers?.join(" ") || "Loop command rejected");
3092
+ const shouldWake = result?.job ? result.job.immediate !== false : ["loop-now", "loop-resume"].includes(name);
3093
+ if (result?.accepted && shouldWake)
3094
+ await runtime.wake(event);
3095
+ return result;
3096
+ }
3097
+ try {
3098
+ registrations.push(await ctx.session.hook("prompt", async (event) => {
3099
+ if (closed || event.metadata?.opencode_loop_v2 === true || !await ownsSession(event.sessionID))
3100
+ return;
3101
+ if (closed)
3102
+ return;
3103
+ return runtime.onEvent({ kind: "foreground", directory, sessionID: event.sessionID });
3104
+ }));
3105
+ registrations.push(await ctx.session.hook("compaction", async (event) => {
3106
+ if (closed || !await ownsSession(event.sessionID))
3107
+ return;
3108
+ if (!closed)
3109
+ return runtime.onEvent({ kind: "compaction", action: "started", directory, sessionID: event.sessionID });
3110
+ }));
3111
+ registrations.push(await ctx.command.transform((draft) => {
3112
+ registerOpenCode2LoopCommands(draft, { execute });
3113
+ if (typeof draft.add !== "function")
3114
+ return;
3115
+ for (const name of [...OPENCODE_LOOP_V2_PRESET_NAMES, "loop-init"]) {
3116
+ draft.add({
3117
+ name,
3118
+ description: name === "loop-init" ? "Create a missing project-local progress file." : `Native Loop ${name.slice(5)} preset`,
3119
+ execute: (input) => execute({ name, sessionID: input.sessionID, arguments: commandArguments(input) })
3120
+ });
3121
+ }
3122
+ }));
3123
+ await bridge.attach(() => ctx.event.subscribe({ signal: lifecycleAbort.signal }));
3124
+ await appendLoopLog(directory, "v2-native-ready", { host: ctx.app?.version || "unknown" });
3125
+ return dispose;
3126
+ } catch (error) {
3127
+ await dispose().catch(() => {});
3128
+ throw error;
3129
+ }
3130
+ }
3131
+ });
3132
+ export {
3133
+ OpenCodeLoopNativePlugin,
3134
+ OpenCodeLoopNativePlugin as default
3135
+ };