@awak-app/simy-cli 0.6.22 → 0.6.23
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.
- package/README.md +18 -0
- package/package.json +1 -1
- package/src/backend-executable.js +75 -7
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/dev-work.mjs +23 -1
- package/src/desktop-host.js +21 -3
- package/src/desktop-status.js +4 -2
- package/src/dev-work-events.js +32 -3
- package/src/local-task.js +3 -0
- package/src/runner.js +18 -1
package/README.md
CHANGED
|
@@ -236,6 +236,15 @@ SQM checks apply when you explicitly select `@simy`, invoke the SQM skill, or as
|
|
|
236
236
|
After installation, restart Codex and find and install **SIMY** in **Plugins**
|
|
237
237
|
before using `@simy` in a new task.
|
|
238
238
|
|
|
239
|
+
If **Connect SIMY** fails while the CLI is already signed in, check the recovery
|
|
240
|
+
message in connection settings. Finish or retry a pending SIMY update, or complete
|
|
241
|
+
or cancel an organization selection. If SIMY remains paused, use **SIMY → Quit
|
|
242
|
+
SIMY**, reopen it, and reconnect; closing the window alone does not restart the
|
|
243
|
+
background host. Refresh connections and confirm the conversation list returns.
|
|
244
|
+
Diagnostic `provider connection failed` entries distinguish `reason=update`,
|
|
245
|
+
`organization_change`, and `shutdown` without including credentials or transcripts.
|
|
246
|
+
Reconnection does not sign in to Codex CLI or restore revoked automatic continuation.
|
|
247
|
+
|
|
239
248
|
The plugin also includes a non-blocking connection reminder. At task startup,
|
|
240
249
|
resume, and prompt submission it reads the origin-scoped local discovery record
|
|
241
250
|
and checks the loopback CLI health endpoint. It warns once per disconnected
|
|
@@ -593,6 +602,15 @@ preflight and returns the installed version, required version, and a recovery
|
|
|
593
602
|
command to SIMY Web. Update Codex with
|
|
594
603
|
`npm install -g @openai/codex@latest`; update Claude Code with `claude update`.
|
|
595
604
|
|
|
605
|
+
Codex discovery compares installed CLI versions across PATH (including shadowed
|
|
606
|
+
installs) and the known Codex/ChatGPT app bundles on macOS, selecting the newest
|
|
607
|
+
compatible version. Equal versions retain discovery order. A compatible
|
|
608
|
+
`SIMY_CODEX_BIN` override retains priority; a broken or unsupported override falls
|
|
609
|
+
back to automatic discovery. Refresh the connection status after updating Codex;
|
|
610
|
+
each new inspection checks installed versions again. SIMY does not download or
|
|
611
|
+
replace Codex. The desktop connection status and Codex conversation reader both
|
|
612
|
+
require `0.158.0` or later for the verified app-server writer-lock protocol.
|
|
613
|
+
|
|
596
614
|
## Your Desktop executor
|
|
597
615
|
|
|
598
616
|
SIMY CLI is the `desktop` execution target, shown to users as **Your Desktop**.
|
package/package.json
CHANGED
|
@@ -32,11 +32,30 @@ export async function inspectBackendExecutable(
|
|
|
32
32
|
assertActive();
|
|
33
33
|
const policy = versionPolicy(backend);
|
|
34
34
|
const requiredVersion = minimumVersion || policy.minimumVersion;
|
|
35
|
+
const explicitCodex = backend === "codex"
|
|
36
|
+
? (discoveryOptions.environment ?? process.env).SIMY_CODEX_BIN : null;
|
|
35
37
|
candidates ??= [...backendCandidates(backend, discoveryOptions), ...await bundledClaudeCandidates(backend, discoveryOptions)];
|
|
38
|
+
candidates = [...new Set(candidates.filter(Boolean))];
|
|
36
39
|
let fallback = null;
|
|
37
|
-
|
|
40
|
+
// An intentional override retains priority. Broken or unsupported overrides
|
|
41
|
+
// still fall back to automatic discovery, as before.
|
|
42
|
+
if (explicitCodex && candidates.includes(explicitCodex)) {
|
|
43
|
+
const pinned = await inspectBackendExecutable(backend, {
|
|
44
|
+
candidates: [explicitCodex], spawnImpl, timeoutMs, signal, minimumVersion,
|
|
45
|
+
discoveryOptions: { ...discoveryOptions, environment: {} },
|
|
46
|
+
});
|
|
47
|
+
if (pinned.compatible) return pinned;
|
|
48
|
+
if (pinned.available) fallback = pinned;
|
|
49
|
+
candidates = candidates.filter(candidate => candidate !== explicitCodex);
|
|
50
|
+
}
|
|
51
|
+
// Comparing all Codex installs must not turn several slow wrappers into a
|
|
52
|
+
// serial startup delay. Keep concurrency bounded and results in source order.
|
|
53
|
+
const probes = backend === "codex"
|
|
54
|
+
? await probeCandidates(candidates, { spawnImpl, timeoutMs, signal }, assertActive) : null;
|
|
55
|
+
let selected = null;
|
|
56
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
38
57
|
assertActive();
|
|
39
|
-
const probe = await probeExecutable(candidate, { spawnImpl, timeoutMs, signal });
|
|
58
|
+
const probe = probes ? probes[index] : await probeExecutable(candidate, { spawnImpl, timeoutMs, signal });
|
|
40
59
|
assertActive();
|
|
41
60
|
if (!probe.available) continue;
|
|
42
61
|
const installedVersion = parseBackendVersion(probe.output);
|
|
@@ -52,12 +71,15 @@ export async function inspectBackendExecutable(
|
|
|
52
71
|
update_command: policy.updateCommand,
|
|
53
72
|
};
|
|
54
73
|
// An executable that starts may still be too old for the requested feature.
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
if (compatible)
|
|
74
|
+
// Claude retains its existing priority. Codex chooses the newest version;
|
|
75
|
+
// equal versions retain discovery order regardless of probe completion order.
|
|
76
|
+
if (compatible) {
|
|
77
|
+
if (backend !== "codex") return inspection;
|
|
78
|
+
if (!selected || compareCodexVersions(installedVersion, selected.installed_version) > 0) selected = inspection;
|
|
79
|
+
}
|
|
58
80
|
fallback ??= inspection;
|
|
59
81
|
}
|
|
60
|
-
return fallback || {
|
|
82
|
+
return selected || fallback || {
|
|
61
83
|
backend,
|
|
62
84
|
executable: null,
|
|
63
85
|
available: false,
|
|
@@ -88,6 +110,8 @@ export function backendCandidates(backend, {
|
|
|
88
110
|
return [
|
|
89
111
|
environment.SIMY_CODEX_BIN,
|
|
90
112
|
"codex",
|
|
113
|
+
...codexPathCandidates(platform, environment),
|
|
114
|
+
...(platform === "darwin" ? ["/opt/homebrew/bin/codex", "/usr/local/bin/codex"] : []),
|
|
91
115
|
...(platform === "darwin" ? ["/Applications", path.posix.join(home, "Applications")].flatMap(directory =>
|
|
92
116
|
["ChatGPT.app", "Codex.app"].flatMap(app => {
|
|
93
117
|
const resources = path.posix.join(directory, app, "Contents", "Resources");
|
|
@@ -98,6 +122,48 @@ export function backendCandidates(backend, {
|
|
|
98
122
|
];
|
|
99
123
|
}
|
|
100
124
|
|
|
125
|
+
function codexPathCandidates(platform, environment) {
|
|
126
|
+
const paths = platform === "win32" ? path.win32 : path.posix;
|
|
127
|
+
const names = platform === "win32" ? ["codex.exe", "codex.cmd", "codex.bat", "codex"] : ["codex"];
|
|
128
|
+
return (environment.PATH ?? environment.Path ?? "").split(paths.delimiter)
|
|
129
|
+
.map(directory => directory.replace(/^"(.*)"$/, "$1"))
|
|
130
|
+
.filter(directory => paths.isAbsolute(directory))
|
|
131
|
+
.flatMap(directory => names.map(name => paths.join(directory, name)))
|
|
132
|
+
.filter(candidate => { try { return statSync(candidate).isFile(); } catch { return false; } });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function probeCandidates(candidates, options, assertActive) {
|
|
136
|
+
let next = 0;
|
|
137
|
+
const probes = new Array(candidates.length);
|
|
138
|
+
await Promise.all(Array.from({ length: Math.min(4, candidates.length) }, async () => {
|
|
139
|
+
while (next < candidates.length) {
|
|
140
|
+
// Drain every owned probe before reporting cancellation to callers.
|
|
141
|
+
if (options.signal?.aborted) break;
|
|
142
|
+
const index = next++;
|
|
143
|
+
probes[index] = await probeExecutable(candidates[index], options);
|
|
144
|
+
}
|
|
145
|
+
}));
|
|
146
|
+
assertActive();
|
|
147
|
+
return probes;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function compareCodexVersions(left, right) {
|
|
151
|
+
const core = compareVersions(left, right);
|
|
152
|
+
if (core) return core;
|
|
153
|
+
const prerelease = version => version.includes("-") ? version.slice(version.indexOf("-") + 1).split(".") : null;
|
|
154
|
+
const a = prerelease(left), b = prerelease(right);
|
|
155
|
+
if (!a || !b) return Number(!a) - Number(!b);
|
|
156
|
+
for (let index = 0; index < Math.max(a.length, b.length); index++) {
|
|
157
|
+
if (a[index] === b[index]) continue;
|
|
158
|
+
if (a[index] === undefined || b[index] === undefined) return a[index] === undefined ? -1 : 1;
|
|
159
|
+
const numericA = /^\d+$/.test(a[index]), numericB = /^\d+$/.test(b[index]);
|
|
160
|
+
if (numericA && numericB) return Number(a[index]) - Number(b[index]);
|
|
161
|
+
if (numericA !== numericB) return numericA ? -1 : 1;
|
|
162
|
+
return a[index] < b[index] ? -1 : 1;
|
|
163
|
+
}
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
101
167
|
// Claude Desktop can install below <version>/<content hash>/claude.app while
|
|
102
168
|
// an older PATH wrapper still points at <version>/claude.app. Discover only the
|
|
103
169
|
// known app layout; never recursively execute arbitrary files from user folders.
|
|
@@ -167,7 +233,9 @@ function probeExecutable(command, { spawnImpl, timeoutMs, signal }) {
|
|
|
167
233
|
let settled = false, terminating = false;
|
|
168
234
|
let timeout, force, limit;
|
|
169
235
|
let output = Buffer.alloc(0);
|
|
170
|
-
|
|
236
|
+
let child;
|
|
237
|
+
try { child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "pipe"] }); }
|
|
238
|
+
catch { resolve({ available: false, output: "" }); return; }
|
|
171
239
|
const capture = chunk => {
|
|
172
240
|
// Version probes need a tiny prefix. Drain noisy executables without
|
|
173
241
|
// retaining unbounded stdout/stderr in the desktop background process.
|
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.23";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -101,9 +101,23 @@ export async function collectDevWorkEvent({
|
|
|
101
101
|
session_id: sha256(`${provider}:${sessionId.trim()}`),
|
|
102
102
|
source,
|
|
103
103
|
project_id: projectId.toLowerCase(),
|
|
104
|
+
...workflowRun(env),
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* W-02: the independent WorkFlow run that started this session. The SIMY
|
|
110
|
+
* agent sets both variables only on the Provider process it launched for a
|
|
111
|
+
* WorkFlow run (and clears inherited values otherwise); SIMY keeps the link
|
|
112
|
+
* only when the run belongs to the signed-in device owner.
|
|
113
|
+
*/
|
|
114
|
+
export function workflowRun(env = process.env) {
|
|
115
|
+
const workflowId = String(env.SIMY_WORKFLOW_ID || "").trim();
|
|
116
|
+
const runId = String(env.SIMY_WORKFLOW_RUN_ID || "").trim();
|
|
117
|
+
if (!UUID.test(workflowId) || !UUID.test(runId)) return {};
|
|
118
|
+
return { workflow_id: workflowId.toLowerCase(), workflow_run_id: runId.toLowerCase() };
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
/** Best effort: the open PR for the current branch via `gh`, if installed. */
|
|
108
122
|
export async function pullRequestUrl({ cwd, repo, env = process.env, runImpl = run }) {
|
|
109
123
|
const url = await runImpl("gh", ["pr", "view", "--json", "url", "--jq", ".url"], {
|
|
@@ -190,7 +204,15 @@ export async function runDevWorkHook({
|
|
|
190
204
|
env.PLUGIN_DATA?.trim() || env.CLAUDE_PLUGIN_DATA?.trim() || join(sessionRoot, "plugin-data", "simy");
|
|
191
205
|
// The upstream observation is keyed by (Project, repo, HEAD, session): a
|
|
192
206
|
// resend of the same state changes nothing, so skip it (and `gh`) locally.
|
|
193
|
-
const fingerprint = sha256(
|
|
207
|
+
const fingerprint = sha256(
|
|
208
|
+
JSON.stringify([
|
|
209
|
+
event.session_id,
|
|
210
|
+
event.repo,
|
|
211
|
+
event.head_sha,
|
|
212
|
+
event.project_id ?? null,
|
|
213
|
+
event.workflow_run_id ?? null,
|
|
214
|
+
]),
|
|
215
|
+
);
|
|
194
216
|
const statePath = join(pluginData, "dev-work", `${event.session_id}.json`);
|
|
195
217
|
if ((await readState(statePath))?.fingerprint === fingerprint) {
|
|
196
218
|
return { sent: false, reason: "unchanged" };
|
package/src/desktop-host.js
CHANGED
|
@@ -67,6 +67,11 @@ function desktopActionError(action, error) {
|
|
|
67
67
|
if (error?.code === "provider_link_changed") return "SIMY connection changed. Refresh and select the conversation again.";
|
|
68
68
|
if (error?.code === "provider_link_disconnected") return "Reconnect this provider to SIMY before using it.";
|
|
69
69
|
if (["provider_connect", "provider_disconnect"].includes(action)) {
|
|
70
|
+
if (error?.code === 'desktop_stopping') {
|
|
71
|
+
if (error.stoppingReason === 'update') return "SIMY is preparing an update. Finish or retry the update; if it failed, quit SIMY completely, reopen it, then reconnect.";
|
|
72
|
+
if (error.stoppingReason === 'organization_change') return "SIMY is switching organizations. Complete or cancel the organization selection, then reconnect.";
|
|
73
|
+
return "SIMY is pausing new work. Quit SIMY completely, reopen it, then reconnect.";
|
|
74
|
+
}
|
|
70
75
|
if (error?.code === 'desktop_access_required') return "Sign in to your SIMY workspace, then try connecting again.";
|
|
71
76
|
if (['EACCES', 'EPERM', 'EROFS'].includes(error?.code)) return "SIMY could not save the connection settings. Check access to its local settings folder, then retry.";
|
|
72
77
|
if (error?.code === 'ENOSPC') return "SIMY could not save the connection settings. Free disk space, then retry.";
|
|
@@ -301,11 +306,17 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
301
306
|
let organizationAnalysesSuspended = false;
|
|
302
307
|
let organizationAnalysisDrain = Promise.resolve();
|
|
303
308
|
let organizationAnalysisEpoch = 0;
|
|
309
|
+
const requireWorkAdmission = () => {
|
|
310
|
+
const stoppingReason = closed ? 'shutdown'
|
|
311
|
+
: stoppingForUpdate || updater?.acceptsNewWork?.() === false ? 'update'
|
|
312
|
+
: organizationAnalysesSuspended ? 'organization_change' : null;
|
|
313
|
+
if (stoppingReason) throw Object.assign(new Error("SIMY is stopping. Reopen SIMY before starting new work."), { code: "desktop_stopping", stoppingReason });
|
|
314
|
+
};
|
|
304
315
|
const authorizeWork = async expectedScope => {
|
|
305
|
-
|
|
316
|
+
requireWorkAdmission();
|
|
306
317
|
if (agent?.requireDesktopAccess) await agent.requireDesktopAccess();
|
|
307
318
|
const access = await accessSnapshot();
|
|
308
|
-
|
|
319
|
+
requireWorkAdmission();
|
|
309
320
|
requireDesktopAccess(access);
|
|
310
321
|
const scope = accessScope(access);
|
|
311
322
|
if (expectedScope !== undefined && expectedScope !== scope) throw Object.assign(new Error("Desktop account or organization changed."), { code: "desktop_access_required" });
|
|
@@ -1327,7 +1338,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1327
1338
|
const provider = PROVIDER_LINKS.includes(command.provider) ? command.provider : 'unsupported';
|
|
1328
1339
|
const kind = ['Error', 'TypeError', 'RangeError', 'SyntaxError'].includes(error?.name) ? error.name : 'Error';
|
|
1329
1340
|
const locations = [...String(error?.stack || '').matchAll(/(?:desktop-host|agent|provider-links|delivery-loop|analysis-manager)\.m?js:\d{1,6}:\d{1,6}/g)].slice(0, 3).map(match => match[0]).join(',') || 'unknown';
|
|
1330
|
-
|
|
1341
|
+
const reason = code === 'desktop_stopping' && ['shutdown', 'update', 'organization_change'].includes(error?.stoppingReason) ? error.stoppingReason : 'unknown';
|
|
1342
|
+
diagnostics.write(`provider connection failed provider=${provider} operation=${command.action} code=${code} reason=${reason} kind=${kind} locations=${locations}`);
|
|
1343
|
+
// Admission can reject before changeProviderLink runs. Retain that
|
|
1344
|
+
// recovery guidance across the next heartbeat, without overwriting
|
|
1345
|
+
// an in-flight link change on the same provider.
|
|
1346
|
+
if (provider !== 'unsupported' && code === 'desktop_stopping' && !providerLinkBusy.has(provider)) {
|
|
1347
|
+
providerLinkFeedback[provider] = { state: 'error', message: desktopActionError(command.action, error) };
|
|
1348
|
+
}
|
|
1331
1349
|
}
|
|
1332
1350
|
emit({ type: "result", id: command.id, ok: false, ...(error?.code === 'cancelled' || statisticsRequest?.controller.signal.aborted ? { code: 'cancelled' } : {}), message: ["update", "prepare_update_restart"].includes(command.action) && updateProgress ? updateProgress.message : desktopActionError(command.action, error) });
|
|
1333
1351
|
} finally {
|
package/src/desktop-status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
|
|
3
|
-
import { BACKEND_VERSION_POLICIES, inspectBackendExecutable } from "./backend-executable.js";
|
|
3
|
+
import { BACKEND_VERSION_POLICIES, CODEX_WRITER_LOCK_MINIMUM_VERSION, inspectBackendExecutable } from "./backend-executable.js";
|
|
4
4
|
import { isSessionValid } from "./session-store.js";
|
|
5
5
|
|
|
6
6
|
const PROVIDER_STATUS_CACHE_MS = 30_000;
|
|
@@ -170,7 +170,9 @@ export async function inspectProviderStatus(provider, {
|
|
|
170
170
|
const checkedAt = isoNow(now);
|
|
171
171
|
let executable;
|
|
172
172
|
try {
|
|
173
|
-
executable = await inspectExecutable(provider, { spawnImpl, timeoutMs
|
|
173
|
+
executable = await inspectExecutable(provider, { spawnImpl, timeoutMs,
|
|
174
|
+
...(provider === "codex" ? { minimumVersion: CODEX_WRITER_LOCK_MINIMUM_VERSION } : {}),
|
|
175
|
+
});
|
|
174
176
|
} catch {
|
|
175
177
|
return providerStatus("unknown", null, checkedAt, `${label} availability could not be checked.`);
|
|
176
178
|
}
|
package/src/dev-work-events.js
CHANGED
|
@@ -13,7 +13,10 @@ export const DEV_WORK_EVENT_MAX_BYTES = 4096;
|
|
|
13
13
|
const REQUEST_TIMEOUT_MS = 5_000;
|
|
14
14
|
|
|
15
15
|
const REQUIRED_KEYS = ["repo", "branch", "head_sha", "session_id", "source"];
|
|
16
|
-
|
|
16
|
+
// W-02: the independent WorkFlow run that started this coding session. Both
|
|
17
|
+
// or neither; Web/Crew keep it only when it is the device owner's own run.
|
|
18
|
+
const OPTIONAL_KEYS = ["pr_url", "project_id", "workflow_id", "workflow_run_id"];
|
|
19
|
+
const UUID_KEYS = ["project_id", "workflow_id", "workflow_run_id"];
|
|
17
20
|
const ALLOWED_KEYS = new Set([...REQUIRED_KEYS, ...OPTIONAL_KEYS]);
|
|
18
21
|
const SOURCES = new Set(["claude_code", "codex", "other"]);
|
|
19
22
|
const REPO = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}$/;
|
|
@@ -52,14 +55,40 @@ export function normalizeDevWorkEvent(value) {
|
|
|
52
55
|
!SESSION_ID.test(event.session_id) ||
|
|
53
56
|
!SOURCES.has(event.source) ||
|
|
54
57
|
(event.pr_url !== undefined && !PR_URL.test(event.pr_url)) ||
|
|
55
|
-
(event
|
|
58
|
+
UUID_KEYS.some((key) => event[key] !== undefined && !UUID.test(event[key])) ||
|
|
59
|
+
(event.workflow_id === undefined) !== (event.workflow_run_id === undefined)
|
|
56
60
|
) {
|
|
57
61
|
throw invalid();
|
|
58
62
|
}
|
|
59
|
-
|
|
63
|
+
for (const key of UUID_KEYS) {
|
|
64
|
+
if (event[key] !== undefined) event[key] = event[key].toLowerCase();
|
|
65
|
+
}
|
|
60
66
|
return event;
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
/** Environment names the SIMY plugin dev-work hook reads (hooks/dev-work.mjs). */
|
|
70
|
+
export const WORKFLOW_ID_ENV = "SIMY_WORKFLOW_ID";
|
|
71
|
+
export const WORKFLOW_RUN_ID_ENV = "SIMY_WORKFLOW_RUN_ID";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Provider child environment naming the independent WorkFlow run that started
|
|
75
|
+
* it, so the plugin hook can report which run the coding session belongs to.
|
|
76
|
+
* Without a complete run reference the keys are set to undefined, which
|
|
77
|
+
* child_process omits, so a value inherited from this agent's own environment
|
|
78
|
+
* can never attribute an unrelated session to a run.
|
|
79
|
+
*/
|
|
80
|
+
export function workflowRunEnv(reference) {
|
|
81
|
+
const workflowId = String(reference?.workflow_id || "");
|
|
82
|
+
const runId = String(reference?.workflow_run_id || "");
|
|
83
|
+
if (!UUID.test(workflowId) || !UUID.test(runId)) {
|
|
84
|
+
return { [WORKFLOW_ID_ENV]: undefined, [WORKFLOW_RUN_ID_ENV]: undefined };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
[WORKFLOW_ID_ENV]: workflowId.toLowerCase(),
|
|
88
|
+
[WORKFLOW_RUN_ID_ENV]: runId.toLowerCase(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
/**
|
|
64
93
|
* Posts one normalized report to Web. Resolves with the minimal receipt; a
|
|
65
94
|
* disabled gateway (404) or any refusal rejects with a status the local
|
package/src/local-task.js
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
normalizeWorkflowStartIntent,
|
|
48
48
|
} from "./workflow-observation-sender.js";
|
|
49
49
|
import { uploadWorkflowDeliverables } from "./workflow-deliverable-upload.js";
|
|
50
|
+
import { workflowRunEnv } from "./dev-work-events.js";
|
|
50
51
|
|
|
51
52
|
// Weighted tokens (cache reads count at 1/10). A real Claude Code WorkFlow step with the
|
|
52
53
|
// Purpose Charter context measured ~200k weighted tokens on dev, so 100k failed every run.
|
|
@@ -580,6 +581,8 @@ export async function startLocalTask(
|
|
|
580
581
|
TMP: runtimeTempRoot,
|
|
581
582
|
TEMP: runtimeTempRoot,
|
|
582
583
|
...command.env,
|
|
584
|
+
// W-02: lets the dev-work hook link this session to its WorkFlow run.
|
|
585
|
+
...workflowRunEnv(task.workflow_observation_binding),
|
|
583
586
|
},
|
|
584
587
|
stdio: ["ignore", "pipe", "pipe"],
|
|
585
588
|
detached: process.platform !== "win32",
|
package/src/runner.js
CHANGED
|
@@ -55,6 +55,7 @@ import { acquireDesktopProcessLock, PROCESS_CONFLICT_CODE } from "./desktop-proc
|
|
|
55
55
|
import {
|
|
56
56
|
assertRecoveryActor, completedWorkerResult, recoveryActorBinding, refreshRecoveryAudit, refreshRecoveryBudgetAudit,
|
|
57
57
|
} from "./coding-run-recovery.js";
|
|
58
|
+
import { workflowRunEnv } from "./dev-work-events.js";
|
|
58
59
|
|
|
59
60
|
const execFileAsync = promisify(execFile);
|
|
60
61
|
const MAX_LOCAL_LOG_LINES = 1_000;
|
|
@@ -1035,6 +1036,17 @@ function sqmEvidenceEndpoint(session) {
|
|
|
1035
1036
|
return new URL("sqm/run-evidence", session.api_base_url).href;
|
|
1036
1037
|
}
|
|
1037
1038
|
|
|
1039
|
+
/**
|
|
1040
|
+
* The independent WorkFlow run this coding run belongs to: the bound run, or
|
|
1041
|
+
* the run a start intent already had admitted by an earlier attempt.
|
|
1042
|
+
*/
|
|
1043
|
+
export function workflowRunReference(run) {
|
|
1044
|
+
const binding = run?.request?.workflow_observation_binding;
|
|
1045
|
+
if (binding) return binding;
|
|
1046
|
+
const observation = run?.snapshot?.metadata?.workflow_observation;
|
|
1047
|
+
return ["accepted", "already_recorded"].includes(observation?.status) ? observation : null;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1038
1050
|
export async function executeProcessAttempt({
|
|
1039
1051
|
backend,
|
|
1040
1052
|
instruction,
|
|
@@ -1080,7 +1092,12 @@ export async function executeProcessAttempt({
|
|
|
1080
1092
|
const checkpointWrites = [];
|
|
1081
1093
|
const child = command.spawn(command.bin, command.args, {
|
|
1082
1094
|
cwd: repositoryPath,
|
|
1083
|
-
env: {
|
|
1095
|
+
env: {
|
|
1096
|
+
...process.env,
|
|
1097
|
+
...command.env,
|
|
1098
|
+
// W-02: lets the dev-work hook link this session to its WorkFlow run.
|
|
1099
|
+
...workflowRunEnv(workflowRunReference(run)),
|
|
1100
|
+
},
|
|
1084
1101
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1085
1102
|
detached: process.platform !== "win32",
|
|
1086
1103
|
});
|