@proagentstore/cli 0.4.97 → 0.4.99

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.
@@ -46,6 +46,18 @@ export const LOCAL_APPLY_HANDOFF_FRAME_PATH = "/local-apply/handoff/frame";
46
46
  export const LOCAL_APPLY_HANDOFF_INPUT_PATH = "/local-apply/handoff/input";
47
47
  export const LOCAL_APPLY_HANDOFF_RESUME_PATH = "/local-apply/handoff/resume";
48
48
  export const LOCAL_APPLY_HANDOFF_END_PATH = "/local-apply/handoff/end";
49
+ /**
50
+ * A separate, browser-only receipt/history check for an already-ended uncertain attempt. This is
51
+ * deliberately not an application run: it receives no engine, sources, artifacts or submit gate.
52
+ */
53
+ export const LOCAL_APPLY_RECONCILIATION_RUN_PATH = "/local-apply/reconciliation/run";
54
+ export const LOCAL_APPLY_RECONCILIATION_STATUS_PATH = "/local-apply/reconciliation/status";
55
+ export const LOCAL_APPLY_RECONCILIATION_HANDOFF_PATH = "/local-apply/reconciliation/handoff";
56
+ export const LOCAL_APPLY_RECONCILIATION_HANDOFF_STATUS_PATH = "/local-apply/reconciliation/handoff-status";
57
+ export const LOCAL_APPLY_RECONCILIATION_HANDOFF_FRAME_PATH = "/local-apply/reconciliation/handoff/frame";
58
+ export const LOCAL_APPLY_RECONCILIATION_HANDOFF_INPUT_PATH = "/local-apply/reconciliation/handoff/input";
59
+ export const LOCAL_APPLY_RECONCILIATION_HANDOFF_RESUME_PATH = "/local-apply/reconciliation/handoff/resume";
60
+ export const LOCAL_APPLY_RECONCILIATION_HANDOFF_END_PATH = "/local-apply/reconciliation/handoff/end";
49
61
  export const LOCAL_APPLY_ENGINES = ["claude", "codex"];
50
62
  export const LOCAL_APPLY_AUTH_MODES = ["machine", "subscription"];
51
63
  export const LOCAL_APPLY_MODES = ["fill_and_review", "auto_submit"];
@@ -10,6 +10,20 @@ export function createLocalApplyBrowserFactory(options) {
10
10
  return { ...browser, handoffPage: options.sharedPage };
11
11
  };
12
12
  }
13
+ /**
14
+ * Keep local-apply's browser construction out of `runner.ts`: research uses the generic factory,
15
+ * while application runs get a page-owning isolated context for the scoped handoff.
16
+ */
17
+ export function createRunnerBrowserFactories(options) {
18
+ const browserFor = async (profile, runDir) => {
19
+ if (profile === "default")
20
+ return { tools: await options.defaultTools(), stop: async () => undefined };
21
+ const mcp = new McpRuntime();
22
+ await mcp.start({ userDataDir: join(runDir, "profile"), headless: options.headless });
23
+ return { tools: mcp, stop: () => mcp.stop() };
24
+ };
25
+ return { browserFor, applyBrowserFor: createLocalApplyBrowserFactory({ headless: options.headless, defaultBrowser: browserFor, sharedPage: options.sharedPage }) };
26
+ }
13
27
  /**
14
28
  * Start the disposable browser context for one application run and retain only its current live
15
29
  * page for the bounded takeover adapter. This deliberately does not seed, copy, or persist an
@@ -0,0 +1,24 @@
1
+ /** Separate from apply and generic tasks: this surface has no bridge/fill/submit operation. */
2
+ export async function routeLocalApplyReconciliation(runtime, method, path, read) {
3
+ if (method !== "POST")
4
+ return undefined;
5
+ if (path === "/local-apply/reconciliation/run")
6
+ return runtime.start(await read());
7
+ if (path === "/local-apply/reconciliation/status")
8
+ return runtime.status(await read());
9
+ if (path === "/local-apply/reconciliation/handoff")
10
+ return runtime.handoff(await read());
11
+ if (path === "/local-apply/reconciliation/handoff-status")
12
+ return runtime.status(await read());
13
+ if (path === "/local-apply/reconciliation/handoff/frame")
14
+ return runtime.handoffFrame(await read());
15
+ if (path === "/local-apply/reconciliation/handoff/input") {
16
+ await runtime.handoffInput(await read());
17
+ return { ok: true };
18
+ }
19
+ if (path === "/local-apply/reconciliation/handoff/resume")
20
+ return runtime.resume(await read());
21
+ if (path === "/local-apply/reconciliation/handoff/end")
22
+ return runtime.end(await read());
23
+ return undefined;
24
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Read-only reconciliation for an ended `submit_unconfirmed` attempt.
3
+ *
4
+ * This is intentionally independent of ApplyBridge: it launches no CLI, receives no source or
5
+ * artifact paths and has no action surface capable of filling, uploading or submitting. The only
6
+ * page inspection remains in this process and returns a closed evidence vocabulary to PAGS.
7
+ */
8
+ import { join } from "node:path";
9
+ import { RunnerInputError } from "../errors.js";
10
+ import { domainWithin } from "../local-browser/bridge.js";
11
+ const SAFE_ID = /^[A-Za-z0-9_.:-]{1,300}$/;
12
+ const TTL_MS = 10 * 60_000;
13
+ /** Dedicated runtime; there is deliberately no bridge, engine, or generic browser-task fallback. */
14
+ export class LocalApplyReconciliationRuntime {
15
+ deps;
16
+ sessions = new Map();
17
+ now;
18
+ schedule;
19
+ constructor(deps) {
20
+ this.deps = deps;
21
+ this.now = deps.now ?? (() => Date.now());
22
+ this.schedule = deps.schedule ?? ((task, delayMs) => {
23
+ const timer = setTimeout(task, delayMs);
24
+ return () => clearTimeout(timer);
25
+ });
26
+ }
27
+ async start(raw) {
28
+ const envelope = parseEnvelope(raw);
29
+ const existing = this.sessions.get(envelope.reconciliationId);
30
+ if (existing) {
31
+ if (existing.envelope.runId !== envelope.runId || existing.envelope.applicationId !== envelope.applicationId || existing.envelope.instanceId !== envelope.instanceId || existing.envelope.browserProfile !== envelope.browserProfile)
32
+ throw new RunnerInputError("The reconciliation id is already bound to another attempt", 409);
33
+ return this.view(existing);
34
+ }
35
+ const browser = await this.deps.browserFor(envelope.browserProfile, join(this.deps.dataDir, "reconciliation", envelope.reconciliationId));
36
+ const session = { envelope, browser, state: "running", expiresAt: this.now() + TTL_MS };
37
+ this.sessions.set(envelope.reconciliationId, session);
38
+ // The deadline belongs to the Runner, not to Console polling. It is installed before
39
+ // navigation so an abandoned client cannot retain an owned browser/liveWork indefinitely.
40
+ session.cancelDeadline = this.schedule(() => { void this.expire(session); }, TTL_MS);
41
+ try {
42
+ const page = this.page(session);
43
+ if (!page)
44
+ return await this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
45
+ await page.goto(envelope.applicationUrl, { waitUntil: "domcontentloaded", timeout: 20_000 });
46
+ return await this.inspect(session);
47
+ }
48
+ catch {
49
+ return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
50
+ }
51
+ }
52
+ async status(raw) {
53
+ const session = this.require(raw);
54
+ if (session.state === "ended")
55
+ return this.view(session);
56
+ if (session.expiresAt <= this.now())
57
+ return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
58
+ if (!this.page(session))
59
+ return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
60
+ if (session.handoff?.state === "ready" && (session.handoff.expiresAt <= this.now() || !this.deps.takeover || await this.deps.takeover.state(session.handoff.id) !== "ready")) {
61
+ await this.closeHandoff(session, session.handoff.expiresAt <= this.now() ? "expired" : "page_lost");
62
+ return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
63
+ }
64
+ return this.view(session);
65
+ }
66
+ async handoff(raw) {
67
+ const request = this.handoffRequest(raw);
68
+ const session = this.require(request);
69
+ this.assertHandoffBinding(session, request);
70
+ if (session.state !== "paused" || !session.pauseReason)
71
+ throw new RunnerInputError("Reconciliation handoff is available only for a live login or CAPTCHA pause", 409);
72
+ if (session.handoff)
73
+ throw new RunnerInputError("A reconciliation handoff already exists", 409);
74
+ const page = this.page(session);
75
+ if (!page || !this.deps.takeover)
76
+ return this.closedHandoff(session, request.handoffId, "profile_unavailable");
77
+ try {
78
+ await this.deps.takeover.open(request, page);
79
+ }
80
+ catch {
81
+ return this.closedHandoff(session, request.handoffId, "page_lost");
82
+ }
83
+ session.handoff = { id: request.handoffId, expiresAt: this.now() + TTL_MS, state: "ready" };
84
+ return this.view(session);
85
+ }
86
+ async handoffFrame(raw) {
87
+ const session = await this.liveHandoff(raw);
88
+ try {
89
+ return await this.deps.takeover.frame(session.handoff.id);
90
+ }
91
+ catch {
92
+ await this.closeHandoff(session, "page_lost");
93
+ await this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
94
+ throw new RunnerInputError("The reconciliation page is no longer available", 409);
95
+ }
96
+ }
97
+ async handoffInput(raw) {
98
+ const request = raw && typeof raw === "object" ? raw : {};
99
+ const session = await this.liveHandoff(request);
100
+ if (!request.input || typeof request.input !== "object")
101
+ throw new RunnerInputError("handoff input is required");
102
+ // A person may authenticate/solve a CAPTCHA, but once that wall is gone this session accepts
103
+ // no further controls. It can never become an application fill/submit surface.
104
+ const blocker = await this.blocker(this.page(session));
105
+ if (!blocker)
106
+ throw new RunnerInputError("The read-only reconciliation handoff no longer accepts browser input", 409);
107
+ try {
108
+ await this.deps.takeover.input(session.handoff.id, request.input);
109
+ }
110
+ catch {
111
+ await this.closeHandoff(session, "page_lost");
112
+ await this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
113
+ throw new RunnerInputError("The reconciliation page is no longer available", 409);
114
+ }
115
+ }
116
+ async resume(raw) {
117
+ const session = await this.liveHandoff(raw);
118
+ return this.inspect(session);
119
+ }
120
+ async end(raw) {
121
+ const request = this.handoffRequest(raw);
122
+ const session = this.require(request);
123
+ this.assertHandoffBinding(session, request);
124
+ if (session.handoff?.id !== request.handoffId)
125
+ throw new RunnerInputError("The reconciliation handoff is closed", 409);
126
+ if (session.handoff?.state === "ready")
127
+ await this.closeHandoff(session, "unavailable");
128
+ return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
129
+ }
130
+ liveWork() { return [...this.sessions.values()].filter((s) => s.state !== "ended").map((s) => `reconciliation:${s.envelope.reconciliationId}`); }
131
+ closeAll() { for (const session of this.sessions.values())
132
+ void this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" }); }
133
+ async inspect(session) {
134
+ const page = this.page(session);
135
+ if (!page)
136
+ return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
137
+ const state = await this.pageState(page).catch(() => null);
138
+ if (!state || !this.permitted(session, state.url))
139
+ return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
140
+ if (state.login || state.captcha) {
141
+ session.state = "paused";
142
+ session.pauseReason = state.captcha ? "captcha" : "login_required";
143
+ return this.view(session);
144
+ }
145
+ // A handoff which has just cleared authentication is the one case where we retain the
146
+ // bounded browser. It lets the owner inspect their authenticated account/job view without
147
+ // turning generic page prose into an outcome. Explicit end or the ten-minute deadline
148
+ // resolves it as ambiguous unless a future provider-specific verifier supplies exact proof.
149
+ if (session.handoff?.state === "ready") {
150
+ session.state = "running";
151
+ session.pauseReason = undefined;
152
+ return this.view(session);
153
+ }
154
+ // SEEK has no validated account/job receipt or history contract in this runtime. Generic
155
+ // prose, receipt-like URLs, unauthenticated pages and absent receipts prove nothing.
156
+ return this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
157
+ }
158
+ async expire(session) {
159
+ if (session.state !== "ended" && session.expiresAt <= this.now())
160
+ await this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
161
+ }
162
+ async finish(session, result) {
163
+ if (session.finishing)
164
+ return session.finishing;
165
+ if (session.state === "ended")
166
+ return this.view(session);
167
+ // Set terminal state synchronously, before any async teardown. Concurrent End/timeout/page
168
+ // loss callers then converge on the same result and liveWork stops advertising ownership.
169
+ session.state = "ended";
170
+ session.pauseReason = undefined;
171
+ session.result = result;
172
+ session.cancelDeadline?.();
173
+ session.cancelDeadline = undefined;
174
+ session.finishing = (async () => {
175
+ if (session.handoff?.state === "ready")
176
+ await this.closeHandoff(session, "unavailable");
177
+ await session.browser.stop().catch(() => undefined);
178
+ return this.view(session);
179
+ })();
180
+ return session.finishing;
181
+ }
182
+ async closeHandoff(session, reason) {
183
+ if (!session.handoff || session.handoff.state === "closed")
184
+ return;
185
+ session.handoff.state = "closed";
186
+ session.handoff.reason = reason;
187
+ await this.deps.takeover?.end(session.handoff.id).catch(() => undefined);
188
+ }
189
+ async closedHandoff(session, id, reason) {
190
+ session.handoff = { id, expiresAt: this.now(), state: "closed", reason };
191
+ return this.finish(session, { state: "unavailable", proofKind: "authorized_profile_unavailable" });
192
+ }
193
+ page(session) { const page = session.browser.handoffPage?.(); return page && !page.isClosed() ? page : null; }
194
+ permitted(session, raw) { try {
195
+ const host = new URL(raw).hostname.toLowerCase();
196
+ return session.envelope.allowDomains.some((d) => domainWithin(host, d));
197
+ }
198
+ catch {
199
+ return false;
200
+ } }
201
+ async blocker(page) { const state = page ? await this.pageState(page).catch(() => null) : null; return !!state && (state.login || state.captcha); }
202
+ async pageState(page) {
203
+ return page.evaluate(() => {
204
+ const text = (document.body?.innerText ?? "").toLowerCase();
205
+ const captcha = !!document.querySelector('iframe[src*="hcaptcha.com"], .h-captcha, iframe[src*="challenges.cloudflare.com"], .cf-turnstile, iframe[src*="arkoselabs"], .geetest_holder') || /confirm (that )?you('?re| are) not a robot|i'?m not a robot|verify (that )?you('?re| are) (a )?human|checking (if the site connection is secure|your browser)/.test(text);
206
+ // Kept in the page: site prose never crosses the runner boundary. This is deliberately
207
+ // narrower than a guess; anything unrecognised becomes `ambiguous`.
208
+ const password = !!document.querySelector("input[type=password]");
209
+ const otp = !!document.querySelector("input[autocomplete='one-time-code'], input[name*='otp' i], input[name*='verification' i], input[id*='otp' i], input[id*='verification' i]");
210
+ const emailSignIn = !!document.querySelector("form[action*='login' i] input[type=email], form[action*='sign-in' i] input[type=email], [data-auth-screen] input[type=email]");
211
+ return { url: location.href, login: password || otp || emailSignIn, captcha };
212
+ });
213
+ }
214
+ require(raw) { const id = typeof raw?.reconciliationId === "string" ? raw.reconciliationId : ""; if (!SAFE_ID.test(id))
215
+ throw new RunnerInputError("reconciliationId is required"); const session = this.sessions.get(id); if (!session)
216
+ throw new RunnerInputError("Reconciliation session not found", 404); return session; }
217
+ handoffRequest(raw) { const o = raw && typeof raw === "object" ? raw : {}; const handoffId = typeof o.handoffId === "string" ? o.handoffId : ""; const runId = typeof o.runId === "string" ? o.runId : ""; const applicationId = typeof o.applicationId === "string" ? o.applicationId : ""; const reconciliationId = typeof o.reconciliationId === "string" ? o.reconciliationId : ""; const browserProfile = o.browserProfile === "isolated" || o.browserProfile === "default" ? o.browserProfile : null; if (![handoffId, runId, applicationId, reconciliationId].every((v) => SAFE_ID.test(v)) || !browserProfile)
218
+ throw new RunnerInputError("reconciliation handoff needs opaque ids and browserProfile"); return { handoffId, runId, applicationId, reconciliationId, browserProfile }; }
219
+ assertHandoffBinding(session, request) { const e = session.envelope; if (e.reconciliationId !== request.reconciliationId || e.runId !== request.runId || e.applicationId !== request.applicationId || e.browserProfile !== request.browserProfile)
220
+ throw new RunnerInputError("The reconciliation handoff does not match this exact attempt", 403); }
221
+ async liveHandoff(raw) { const request = this.handoffRequest(raw); const session = this.require(request); this.assertHandoffBinding(session, request); if ((session.state !== "paused" && session.state !== "running") || session.handoff?.id !== request.handoffId || session.handoff.state !== "ready")
222
+ throw new RunnerInputError("The reconciliation handoff is closed", 409); if (session.handoff.expiresAt <= this.now()) {
223
+ await this.closeHandoff(session, "expired");
224
+ await this.finish(session, { state: "ambiguous", proofKind: "ambiguous_site_history" });
225
+ throw new RunnerInputError("The reconciliation handoff is closed", 409);
226
+ } return session; }
227
+ view(session) { const e = session.envelope; return { reconciliationId: e.reconciliationId, runId: e.runId, applicationId: e.applicationId, browserProfile: e.browserProfile, context: "separate_read_only_context", state: session.state, ...(session.pauseReason ? { pauseReason: session.pauseReason } : {}), ...(session.result ? { result: session.result } : {}) }; }
228
+ }
229
+ function parseEnvelope(raw) {
230
+ const o = raw && typeof raw === "object" ? raw : {};
231
+ const ids = ["reconciliationId", "runId", "instanceId", "applicationId"];
232
+ for (const key of ids)
233
+ if (typeof o[key] !== "string" || !SAFE_ID.test(o[key]))
234
+ throw new RunnerInputError(`Invalid reconciliation: ${key} is required`);
235
+ if (o.type !== "local_browser.apply.reconciliation" || (o.browserProfile !== "isolated" && o.browserProfile !== "default") || typeof o.applicationUrl !== "string" || !/^https?:\/\/[^\s/]+/i.test(o.applicationUrl))
236
+ throw new RunnerInputError("Invalid read-only reconciliation envelope");
237
+ const allowDomains = Array.isArray(o.allowDomains) ? o.allowDomains.filter((d) => typeof d === "string" && /^[a-z0-9.-]+$/i.test(d)).map((d) => d.toLowerCase()) : [];
238
+ if (!allowDomains.length || typeof o.jobIdentity !== "string" || !o.materialFingerprint || typeof o.materialFingerprint !== "object")
239
+ throw new RunnerInputError("Invalid reconciliation binding");
240
+ return { type: o.type, reconciliationId: o.reconciliationId, runId: o.runId, instanceId: o.instanceId, applicationId: o.applicationId, browserProfile: o.browserProfile, applicationUrl: o.applicationUrl, allowDomains, jobIdentity: o.jobIdentity, materialFingerprint: o.materialFingerprint };
241
+ }
@@ -17,8 +17,9 @@ import { LOCAL_BROWSER_TASK_TYPE } from "./local-browser/contract.js";
17
17
  import { LocalArtifactRuntime } from "./local-artifact/runtime.js";
18
18
  import { LOCAL_ARTIFACT_TASK_TYPE } from "./local-artifact/contract.js";
19
19
  import { LocalApplyRuntime } from "./local-apply/runtime.js";
20
+ import { LocalApplyReconciliationRuntime } from "./local-apply/reconciliation-runtime.js";
20
21
  import { LOCAL_APPLY_TASK_TYPE } from "./local-apply/contract.js";
21
- import { createLocalApplyBrowserFactory } from "./local-apply/live-run-browser.js";
22
+ import { createRunnerBrowserFactories } from "./local-apply/live-run-browser.js";
22
23
  import { createLocalApplyTakeoverAdapter, isLocalApplyTakeover } from "./local-apply/takeover-adapter.js";
23
24
  import { WORKFLOW_DRIVEN_TASKS } from "./task-types.js";
24
25
  /** True for a plain object. */
@@ -87,6 +88,8 @@ export class LocalRunner {
87
88
  localArtifact;
88
89
  /** Local application execution (#957): a signed-in CLI filling one application through the apply bridge. */
89
90
  localApply;
91
+ /** Browser-only, no-bridge reconciliation for an ended uncertain apply attempt. */
92
+ localApplyReconciliation;
90
93
  /** This runner's own local URL, once the server listens — the bridge forwarder calls back to it. */
91
94
  selfUrl = null;
92
95
  /** Live human-takeover sessions, keyed by task id (the page is kept alive). */
@@ -96,24 +99,17 @@ export class LocalRunner {
96
99
  mkdirSync(config.dataDir, { recursive: true });
97
100
  this.store = new RunnerStore(config.dataDir);
98
101
  this.coding = new CodingRuntime(join(config.dataDir, "repos"));
99
- // `default` is the runner's own signed-in browser, shared and never closed by a run;
100
- // `isolated` is a fresh profile inside the run's folder, removed with it.
101
- const browserFor = async (profile, runDir) => {
102
- if (profile === "default")
103
- return { tools: await this.getMcp(), stop: async () => undefined };
104
- const mcp = new McpRuntime();
105
- await mcp.start({ userDataDir: join(runDir, "profile"), headless: config.headless });
106
- return { tools: mcp, stop: () => mcp.stop() };
107
- };
108
- const applyBrowserFor = createLocalApplyBrowserFactory({ headless: config.headless, defaultBrowser: browserFor, sharedPage: () => this.activePage });
102
+ const { browserFor, applyBrowserFor } = createRunnerBrowserFactories({ headless: config.headless, defaultTools: () => this.getMcp(), sharedPage: () => this.activePage });
109
103
  this.localBrowser = new LocalBrowserRuntime({ dataDir: config.dataDir, selfUrl: () => this.selfUrl, browserFor });
110
104
  this.localArtifact = new LocalArtifactRuntime({ dataDir: config.dataDir });
105
+ const localApplyTakeover = createLocalApplyTakeoverAdapter({ get: (id) => this.takeovers.get(id), set: (id, page) => this.takeovers.set(id, { page, reason: "local_apply", humanDone: false }), frame: (id) => this.takeoverFrame(id), input: (id, input) => this.takeoverInput(id, input), end: (id) => this.endTakeover(id) });
111
106
  this.localApply = new LocalApplyRuntime({
112
107
  dataDir: config.dataDir,
113
108
  selfUrl: () => this.selfUrl,
114
109
  browserFor: applyBrowserFor,
115
- takeover: createLocalApplyTakeoverAdapter({ get: (id) => this.takeovers.get(id), set: (id, page) => this.takeovers.set(id, { page, reason: "local_apply", humanDone: false }), frame: (id) => this.takeoverFrame(id), input: (id, input) => this.takeoverInput(id, input), end: (id) => this.endTakeover(id) }),
110
+ takeover: localApplyTakeover,
116
111
  });
112
+ this.localApplyReconciliation = new LocalApplyReconciliationRuntime({ dataDir: config.dataDir, browserFor: applyBrowserFor, takeover: localApplyTakeover });
117
113
  // Tasks paused/running on a previous process are orphaned now — their
118
114
  // pages and takeover sessions are gone. Fail them so the board is clean.
119
115
  const expired = this.store.expireInFlightTasks();
@@ -131,7 +127,7 @@ export class LocalRunner {
131
127
  */
132
128
  liveWork() {
133
129
  const coding = this.coding.liveWork();
134
- const local = [...this.localBrowser.liveWork(), ...this.localArtifact.liveWork(), ...this.localApply.liveWork()];
130
+ const local = [...this.localBrowser.liveWork(), ...this.localArtifact.liveWork(), ...this.localApply.liveWork(), ...this.localApplyReconciliation.liveWork()];
135
131
  return { codingTurns: coding.length, localRuns: local.length, detail: [...coding, ...local].slice(0, 10) };
136
132
  }
137
133
  capabilities() {
@@ -292,6 +288,7 @@ export class LocalRunner {
292
288
  this.localBrowser.closeAll();
293
289
  this.localArtifact.closeAll();
294
290
  this.localApply.closeAll();
291
+ this.localApplyReconciliation.closeAll();
295
292
  try {
296
293
  this.coding.closeAll();
297
294
  }
@@ -5,6 +5,7 @@ import { probeGitSshIdentity } from "./coding/repo.js";
5
5
  import { checkEngine } from "./coding/engine-check.js";
6
6
  import { listGithubOrgs, listGithubRepos, searchGithubRepos, getGithubRepoDetail, getGithubCredentialScope } from "./coding/github-browse.js";
7
7
  import { routeLocalApplyHandoff } from "./local-apply/handoff-routes.js";
8
+ import { routeLocalApplyReconciliation } from "./local-apply/reconciliation-routes.js";
8
9
  export function createRunnerServer(runner) {
9
10
  return createServer(async (req, res) => {
10
11
  try {
@@ -239,6 +240,9 @@ async function route(runner, req, res) {
239
240
  const handoff = await routeLocalApplyHandoff(runner.localApply, req.method, path, () => readJson(req));
240
241
  if (handoff)
241
242
  return json(res, 200, handoff);
243
+ const reconciliation = await routeLocalApplyReconciliation(runner.localApplyReconciliation, req.method, path, () => readJson(req));
244
+ if (reconciliation !== undefined)
245
+ return json(res, 200, reconciliation);
242
246
  if (req.method === "POST" && path === "/local-apply/resume") {
243
247
  return json(res, 200, runner.localApply.resume(await readJson(req)));
244
248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.97",
3
+ "version": "0.4.99",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",