lossless-openclaw-orchestrator 1.1.1 → 1.1.3

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 CHANGED
@@ -14,7 +14,7 @@ safe action without rereading raw transcripts.
14
14
  [![License](https://img.shields.io/badge/license-PolyForm%20Noncommercial-blue)](LICENSE)
15
15
  [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](CONTRIBUTING.md)
16
16
 
17
- [Setup](docs/SETUP.md) · [Contributing](CONTRIBUTING.md) · [Agent Instructions](AGENTS.md) · [Agent Skill](skills/lossless-openclaw-orchestrator/SKILL.md) · [OpenClaw Plugin](docs/OPENCLAW_PLUGIN.md) · [Security](SECURITY.md) · [Code of Conduct](CODE_OF_CONDUCT.md) · [Vision](VISION.md) · [Privacy](docs/PRIVACY.md) · [Claude Boundary](docs/CLAUDE_ADAPTER_BOUNDARY.md) · [Claim Audit](docs/CLAIM_AUDIT.md) · [Release Notes](docs/RELEASE_NOTES_1.1.1.md) · [1.0 Notes](docs/RELEASE_NOTES_1.0.0.md) · [License](LICENSE)
17
+ [Setup](docs/SETUP.md) · [Contributing](CONTRIBUTING.md) · [Agent Instructions](AGENTS.md) · [Agent Skill](skills/lossless-openclaw-orchestrator/SKILL.md) · [OpenClaw Plugin](docs/OPENCLAW_PLUGIN.md) · [Security](SECURITY.md) · [Code of Conduct](CODE_OF_CONDUCT.md) · [Vision](VISION.md) · [Privacy](docs/PRIVACY.md) · [Claude Boundary](docs/CLAUDE_ADAPTER_BOUNDARY.md) · [Claim Audit](docs/CLAIM_AUDIT.md) · [Release Notes](docs/RELEASE_NOTES_1.1.3.md) · [1.0 Notes](docs/RELEASE_NOTES_1.0.0.md) · [License](LICENSE)
18
18
 
19
19
  ## Why It Matters
20
20
 
package/VISION.md CHANGED
@@ -10,9 +10,9 @@ The stable product should feel like a local orchestration cockpit: OpenClaw can
10
10
 
11
11
  ## Current Milestone: 1.1 Desktop Collaboration Cockpit
12
12
 
13
- The current release lane is [#393](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/393), which publishes stable `1.1.1` on npm `latest` after the same full check, smoke, scenario, scorecard, release-status, finalization, and post-publish readiness gates used for `1.1.0`. This patch carries the same stable claim scope while adding the #380 same-connection live-control safety fix, the #385 Desktop-first agent skill wording fix, and the #382 Desktop-first daily orchestration scenario contract.
13
+ The current release lane is [#396](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/396), which publishes stable `1.1.2` on npm `latest` after the same full check, smoke, scenario, scorecard, release-status, finalization, and post-publish readiness gates used for `1.1.1`. This patch carries the same stable claim scope while adding the #381 issue-grade public-safe live-control smoke failure report.
14
14
 
15
- The stable 1.0.0 and 1.1.0 packages have shipped on npm `latest` and their GitHub Releases are published. The post-GA Desktop claim-validation lane [#306](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/306) records that Desktop-visible classification and fallback readiness/status are proven, while actual Codex GUI mutation remains excluded. Desktop parity [#307](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/307) added the coherence classifier; desktop fallback [#308](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/308) added the CUA-first and Peekaboo-secondary readiness report. The active product tracker is the 1.1 collaboration cockpit [#309](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/309), with the operating loop [#16](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/16).
15
+ The stable 1.0.0, 1.1.0, and 1.1.1 packages have shipped on npm `latest` and their GitHub Releases are published. The post-GA Desktop claim-validation lane [#306](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/306) records that Desktop-visible classification and fallback readiness/status are proven, while actual Codex GUI mutation remains excluded. Desktop parity [#307](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/307) added the coherence classifier; desktop fallback [#308](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/308) added the CUA-first and Peekaboo-secondary readiness report. The active product tracker is the 1.1 collaboration cockpit [#309](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/309), with the operating loop [#16](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/16).
16
16
 
17
17
  The Codex Autonomy Cockpit [#254](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/254) and Eva Operating Picture [#255](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/255) P0 lanes are completed beta foundation, not the current active child-work list. Completed P0 children include shared contracts [#256](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/256), source authority [#258](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/258), watcher/resume requests [#259](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/259), visible Codex map joins [#260](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/260), deterministic GitHub operating inputs [#264](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/264)/[#265](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/265), current-lane source balancing [#269](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/269), GitHub check-state fidelity [#270](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/270), cockpit card cleanup [#271](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/271), and end-to-end Eva cockpit dogfood [#272](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/272). The sprint brief remains the historical handoff for that P0 work: [docs/sprints/brief-lco-codex-autonomy-cockpit-sprint-2026-07-01.md](docs/sprints/brief-lco-codex-autonomy-cockpit-sprint-2026-07-01.md).
18
18
 
@@ -20,7 +20,7 @@ The core Codex recall, M9 handoff paths, Codex Autonomy Cockpit, Eva Operating P
20
20
 
21
21
  The current target is:
22
22
 
23
- - Promote npm `latest` from stable `1.1.0` to stable `1.1.1` only after #393 passes full check, smoke, scenario, scorecard, release-status, finalization, and post-publish readiness gates; keep `beta` aligned with beta trains and use `next` for future release candidates.
23
+ - Promote npm `latest` from stable `1.1.1` to stable `1.1.2` only after #396 passes full check, smoke, scenario, scorecard, release-status, finalization, and post-publish readiness gates; keep `beta` aligned with beta trains and use `next` for future release candidates.
24
24
  - Keep README, VISION, release notes, and scorecards current so completed release gates are recorded as completed proof, not active work.
25
25
  - Treat #307/#308 as completed proof for Desktop-visible classification and fallback readiness/status, not as proof of Codex GUI mutation, prompt typing, clicking, refresh/restart automation, or unattended visible collaboration.
26
26
  - Use #309 to design the next collaboration cockpit layer from structured public-safe cards, explicit action-bound proof gates, and clear excluded-claim wording.
@@ -16,7 +16,9 @@ import { runOpenClawGatewayLiveControlSmoke } from "./openclaw-live-control-smok
16
16
  import { runOpenClawPostActionRefreshSmoke } from "./openclaw-post-action-refresh-smoke.js";
17
17
  import { createScorecardSweep } from "./scorecard-sweep.js";
18
18
  import { createScenarioSweep } from "./scenario-sweep.js";
19
+ import { createRuntimeProofIssuePacket } from "./runtime-issue-packet.js";
19
20
  import { createOnboardingStatusReport, writeOnboardingStatusReport } from "./onboarding-status.js";
21
+ import { createRuntimeSweepSummary } from "./runtime-sweep-summary.js";
20
22
  import { normalizeReleaseClaimScope } from "./release-claim-scope.js";
21
23
  import { AppServerLiveControlSmokeClient, runLiveControlSmoke } from "./live-control-smoke.js";
22
24
  import { createLocalMacSearchUiShell, REQUIRED_LOCAL_MAC_SEARCH_UI_TOOLS, sampleLocalMacSearchUiShell, writeLocalMacSearchUiEvidence } from "../../local-mac-ui/src/shell.js";
@@ -380,6 +382,18 @@ async function main() {
380
382
  process.exitCode = 1;
381
383
  return;
382
384
  }
385
+ if (command === "runtime" && args[0] === "sweep-summary") {
386
+ if (hasHelpFlag(args.slice(1))) {
387
+ printRuntimeSweepSummaryHelp();
388
+ return;
389
+ }
390
+ const parsed = parseRuntimeSweepSummaryArgs(args.slice(1));
391
+ const report = createRuntimeSweepSummary(parsed);
392
+ console.log(JSON.stringify(report, null, 2));
393
+ if (parsed.strict && (!report.summaryReady || report.claimBoundary.supportedClaimScope === "none"))
394
+ process.exitCode = 1;
395
+ return;
396
+ }
383
397
  if (command === "ui" && args[0] === "local-mac-search") {
384
398
  if (hasHelpFlag(args.slice(1))) {
385
399
  printLocalMacSearchUiHelp();
@@ -456,6 +470,25 @@ async function main() {
456
470
  process.exitCode = 1;
457
471
  return;
458
472
  }
473
+ if (command === "runtime" && args[0] === "issue-packet") {
474
+ if (hasHelpFlag(args.slice(1))) {
475
+ printRuntimeIssuePacketHelp();
476
+ return;
477
+ }
478
+ const parsed = parseRuntimeIssuePacketArgs(args.slice(1));
479
+ const report = createRuntimeProofIssuePacket({
480
+ evidenceDir: parsed.evidenceDir,
481
+ failureReport: parsed.failureReport,
482
+ parentIssue: parsed.parentIssue,
483
+ operatingLoopIssue: parsed.operatingLoopIssue,
484
+ milestone: parsed.milestone,
485
+ now: parsed.now
486
+ });
487
+ console.log(JSON.stringify(report, null, 2));
488
+ if (parsed.strict && !report.issuePacketReady)
489
+ process.exitCode = 1;
490
+ return;
491
+ }
459
492
  if (command === "release" && args[0] === "preflight") {
460
493
  if (hasHelpFlag(args.slice(1))) {
461
494
  printReleasePreflightHelp();
@@ -750,9 +783,11 @@ function mainUsageText() {
750
783
  " loo openclaw live-control-smoke --evidence-dir path --thread-id id [--openclaw-bin path] [--dev] [--profile name] [--gateway-url ws://127.0.0.1:port] [--token token] [--gateway-timeout-ms ms] [--session-key key] [--message text] [--strict]",
751
784
  " loo openclaw post-action-refresh-smoke --evidence-dir path --thread-id id --live-proof-report path [--openclaw-bin path] [--dev] [--profile name] [--gateway-url ws://127.0.0.1:port] [--token token] [--gateway-timeout-ms ms] [--session-key key] [--query text] [--expand-profile metadata|brief|evidence] [--token-budget n] [--strict]",
752
785
  " loo scorecards sweep --evidence-dir path [--scorecard-dir path] [--claim-scope codex-live-control|codex-read-search-expand-dry-run|codex-working-app-proof] [--strict]",
786
+ " loo runtime sweep-summary --evidence-dir path --dry-run-scenarios path --runtime-scenarios path --scorecard-sweep path --published-smoke path [--runtime-proof-dir path] [--now iso] [--strict]",
753
787
  " loo ui local-mac-search --evidence-dir path [--sample] [--strict]",
754
788
  " loo eval retrieval --scenario-file path [--evidence-path path] [--strict]",
755
789
  " loo eval scenarios --evidence-dir path [--scenario-dir path] [--runtime-proof-dir path] [--strict]",
790
+ " loo runtime issue-packet --evidence-dir path --failure-report path [--parent-issue #n] [--operating-loop #n] [--milestone name] [--now iso] [--strict]",
756
791
  " loo release preflight [--evidence-dir path] [--claim-scope codex-live-control|codex-read-search-expand-dry-run|codex-working-app-proof] [--approved-live-control-evidence path] [--runtime-proof-dir path] [--strict]",
757
792
  " loo release bundle --evidence-dir path [--claim-scope codex-live-control|codex-read-search-expand-dry-run|codex-working-app-proof] [--approved-live-control-evidence path] [--runtime-proof-dir path] [--strict]",
758
793
  " loo release status --evidence-dir path --candidate-sha sha [--claim-scope codex-live-control|codex-read-search-expand-dry-run|codex-working-app-proof] [--approved-live-control-evidence path] [--runtime-proof-dir path] [--npm-publish-approval-evidence path] [--github-release-approval-evidence path] [--github-ci-evidence path] [--codeql-evidence path] [--desktop-gui-required --desktop-gui-approval-evidence path] [--now iso] [--strict]",
@@ -906,6 +941,48 @@ function printScenarioSweepHelp() {
906
941
  " It does not read raw Codex transcripts, run live Codex control, mutate a desktop GUI, publish npm, or create a GitHub Release."
907
942
  ].join("\n"));
908
943
  }
944
+ function printRuntimeIssuePacketHelp() {
945
+ console.log([
946
+ "Usage:",
947
+ " loo runtime issue-packet --evidence-dir path --failure-report path [--parent-issue #n] [--operating-loop #n] [--milestone name] [--now iso] [--strict]",
948
+ "",
949
+ "Writes a public-safe issue-ready handoff packet from a failed runtime proof or scenario sweep report.",
950
+ "",
951
+ "Required:",
952
+ " --evidence-dir is required and receives runtime-proof-issue-packet.json.",
953
+ " --failure-report points to the failed public-safe runtime proof, smoke, or scenario-sweep JSON report.",
954
+ "",
955
+ "Strict mode:",
956
+ " --strict exits non-zero when the failure report is missing, malformed, lacks blocker codes, or when packet redaction fails.",
957
+ "",
958
+ "Safety boundary:",
959
+ " The command never runs gh issue create and never writes to GitHub.",
960
+ " It records only blocker codes, scenario ids, duplicate-check query, acceptance criteria, proof boundary, and redaction categories.",
961
+ " It does not read raw Codex transcripts, run live Codex control, mutate a GUI, publish npm, or create a GitHub Release."
962
+ ].join("\n"));
963
+ }
964
+ function printRuntimeSweepSummaryHelp() {
965
+ console.log([
966
+ "Usage:",
967
+ " loo runtime sweep-summary --evidence-dir path --dry-run-scenarios path --runtime-scenarios path --scorecard-sweep path --published-smoke path [--runtime-proof-dir path] [--now iso] [--strict]",
968
+ "",
969
+ "Writes a public-safe summary that separates dry-run scenario readiness from missing runtime proof markers.",
970
+ "",
971
+ "Required:",
972
+ " --dry-run-scenarios points to the v1 dry-run scenario sweep report.",
973
+ " --runtime-scenarios points to the v1.1 runtime-required scenario sweep report.",
974
+ " --scorecard-sweep points to the working-app scorecard sweep report.",
975
+ " --published-smoke points to the published-package or gateway setup smoke report.",
976
+ "",
977
+ "Strict mode:",
978
+ " --strict exits non-zero when the summary itself cannot be produced safely or no claim scope is supported.",
979
+ " Missing runtime markers remain claim-boundary blockers, not packet-generation failures.",
980
+ "",
981
+ "Safety boundary:",
982
+ " The command consumes public-safe reports only.",
983
+ " It does not read raw Codex transcripts, run live Codex control, mutate a GUI, publish npm, create tags, or create a GitHub Release."
984
+ ].join("\n"));
985
+ }
909
986
  function printOnboardingStatusHelp() {
910
987
  console.log([
911
988
  "Usage:",
@@ -1640,6 +1717,47 @@ function parseScenarioSweepArgs(input) {
1640
1717
  throw new Error("eval scenarios requires --evidence-dir");
1641
1718
  return { evidenceDir, scenarioDir, runtimeProofDir, scenarioIds: scenarioIds.length ? scenarioIds : undefined, strict };
1642
1719
  }
1720
+ function parseRuntimeIssuePacketArgs(input) {
1721
+ let evidenceDir = "";
1722
+ let failureReport = "";
1723
+ let parentIssue;
1724
+ let operatingLoopIssue;
1725
+ let milestone;
1726
+ let now;
1727
+ let strict = false;
1728
+ for (let index = 0; index < input.length; index += 1) {
1729
+ const arg = input[index];
1730
+ if (arg === "--evidence-dir") {
1731
+ evidenceDir = requireOptionValue(input[++index], arg);
1732
+ }
1733
+ else if (arg === "--failure-report") {
1734
+ failureReport = requireOptionValue(input[++index], arg);
1735
+ }
1736
+ else if (arg === "--parent-issue") {
1737
+ parentIssue = requireOptionValue(input[++index], arg);
1738
+ }
1739
+ else if (arg === "--operating-loop") {
1740
+ operatingLoopIssue = requireOptionValue(input[++index], arg);
1741
+ }
1742
+ else if (arg === "--milestone") {
1743
+ milestone = requireOptionValue(input[++index], arg);
1744
+ }
1745
+ else if (arg === "--now") {
1746
+ now = requireOptionValue(input[++index], arg);
1747
+ }
1748
+ else if (arg === "--strict") {
1749
+ strict = true;
1750
+ }
1751
+ else {
1752
+ throw new Error(`Unknown runtime issue-packet option: ${arg}`);
1753
+ }
1754
+ }
1755
+ if (!evidenceDir)
1756
+ throw new Error("runtime issue-packet requires --evidence-dir");
1757
+ if (!failureReport)
1758
+ throw new Error("runtime issue-packet requires --failure-report");
1759
+ return { evidenceDir, failureReport, parentIssue, operatingLoopIssue, milestone, now, strict };
1760
+ }
1643
1761
  function readRetrievalScenarioFile(path) {
1644
1762
  const scenarioPath = resolve(path);
1645
1763
  if (!existsSync(scenarioPath))
@@ -2066,6 +2184,57 @@ function parseScorecardSweepArgs(input) {
2066
2184
  throw new Error("scorecards sweep requires --evidence-dir");
2067
2185
  return { evidenceDir, scorecardDir, claimScope, strict };
2068
2186
  }
2187
+ function parseRuntimeSweepSummaryArgs(input) {
2188
+ let evidenceDir;
2189
+ let dryRunScenarios;
2190
+ let runtimeScenarios;
2191
+ let scorecardSweep;
2192
+ let publishedSmoke;
2193
+ let runtimeProofDir;
2194
+ let now;
2195
+ let strict = false;
2196
+ for (let index = 0; index < input.length; index += 1) {
2197
+ const arg = input[index];
2198
+ if (arg === "--evidence-dir") {
2199
+ evidenceDir = requireOptionValue(input[++index], arg);
2200
+ }
2201
+ else if (arg === "--dry-run-scenarios") {
2202
+ dryRunScenarios = requireOptionValue(input[++index], arg);
2203
+ }
2204
+ else if (arg === "--runtime-scenarios") {
2205
+ runtimeScenarios = requireOptionValue(input[++index], arg);
2206
+ }
2207
+ else if (arg === "--scorecard-sweep") {
2208
+ scorecardSweep = requireOptionValue(input[++index], arg);
2209
+ }
2210
+ else if (arg === "--published-smoke") {
2211
+ publishedSmoke = requireOptionValue(input[++index], arg);
2212
+ }
2213
+ else if (arg === "--runtime-proof-dir") {
2214
+ runtimeProofDir = requireOptionValue(input[++index], arg);
2215
+ }
2216
+ else if (arg === "--now") {
2217
+ now = requireOptionValue(input[++index], arg);
2218
+ }
2219
+ else if (arg === "--strict") {
2220
+ strict = true;
2221
+ }
2222
+ else {
2223
+ throw new Error(`Unknown runtime sweep-summary option: ${arg}`);
2224
+ }
2225
+ }
2226
+ if (!evidenceDir)
2227
+ throw new Error("runtime sweep-summary requires --evidence-dir");
2228
+ if (!dryRunScenarios)
2229
+ throw new Error("runtime sweep-summary requires --dry-run-scenarios");
2230
+ if (!runtimeScenarios)
2231
+ throw new Error("runtime sweep-summary requires --runtime-scenarios");
2232
+ if (!scorecardSweep)
2233
+ throw new Error("runtime sweep-summary requires --scorecard-sweep");
2234
+ if (!publishedSmoke)
2235
+ throw new Error("runtime sweep-summary requires --published-smoke");
2236
+ return { evidenceDir, dryRunScenarios, runtimeScenarios, scorecardSweep, publishedSmoke, runtimeProofDir, now, strict };
2237
+ }
2069
2238
  function parsePositiveInteger(value, name, max) {
2070
2239
  const parsed = Number(value);
2071
2240
  if (!Number.isInteger(parsed) || parsed < 1 || (max !== undefined && parsed > max)) {
@@ -51,11 +51,28 @@ export async function runLiveControlSmoke(options) {
51
51
  mkdirSync(options.evidenceDir, { recursive: true });
52
52
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
53
53
  const message = options.message ?? DEFAULT_MESSAGE;
54
- await options.client.connect();
54
+ let targetState = {
55
+ threadId: options.threadId ?? null,
56
+ source: options.threadId ? "provided_thread" : "unknown"
57
+ };
58
+ let dryRunState = null;
59
+ let liveState = {
60
+ accepted: false,
61
+ approvalAuditId: null,
62
+ method: null,
63
+ completed: false,
64
+ status: null,
65
+ notificationMethods: [],
66
+ approvalRequestCount: 0,
67
+ serverRequestCount: 0
68
+ };
69
+ let failed = false;
55
70
  try {
71
+ await options.client.connect();
56
72
  const target = options.threadId
57
73
  ? { threadId: options.threadId, source: "provided_thread" }
58
74
  : { threadId: await startEphemeralThread(options.client, options.cwd), source: "ephemeral_thread_start" };
75
+ targetState = target;
59
76
  const control = createCodexControl({
60
77
  audit: options.audit,
61
78
  client: {
@@ -74,6 +91,12 @@ export async function runLiveControlSmoke(options) {
74
91
  }
75
92
  });
76
93
  const dryRun = await control.sendMessage({ threadId: target.threadId, message, dryRun: true });
94
+ dryRunState = {
95
+ approvalAuditId: dryRun.approvalAuditId,
96
+ paramsHash: dryRun.paramsHash,
97
+ messageHash: dryRun.messageHash,
98
+ live: false
99
+ };
77
100
  if (!dryRun.messageHash)
78
101
  throw new Error("dry-run did not produce a message hash");
79
102
  const live = await control.sendMessage({
@@ -82,10 +105,24 @@ export async function runLiveControlSmoke(options) {
82
105
  dryRun: false,
83
106
  approvalAuditId: dryRun.approvalAuditId
84
107
  });
108
+ liveState = {
109
+ ...liveState,
110
+ accepted: true,
111
+ approvalAuditId: live.approvalAuditId,
112
+ method: live.method ?? null
113
+ };
85
114
  const turnId = extractTurnId(live.response);
86
115
  if (!turnId)
87
116
  throw new Error("live Codex control response did not include a turn id");
88
117
  const completion = await options.client.waitForTurnCompletion({ threadId: target.threadId, turnId, timeoutMs });
118
+ liveState = {
119
+ ...liveState,
120
+ completed: completion.completed,
121
+ status: completion.status,
122
+ notificationMethods: summarizeMethods(completion.notificationMethods),
123
+ approvalRequestCount: completion.approvalRequestCount,
124
+ serverRequestCount: completion.serverRequestCount
125
+ };
89
126
  if (completion.approvalRequestCount > 0) {
90
127
  throw new Error("live Codex control smoke observed a Codex approval request; harmless smoke must not require approvals");
91
128
  }
@@ -138,8 +175,29 @@ export async function runLiveControlSmoke(options) {
138
175
  writeJson(reportPath, report);
139
176
  return report;
140
177
  }
178
+ catch (error) {
179
+ failed = true;
180
+ try {
181
+ writeFailureReport(options, {
182
+ error,
183
+ target: targetState,
184
+ dryRun: dryRunState,
185
+ live: liveState
186
+ });
187
+ }
188
+ catch {
189
+ // Best-effort evidence: never let report-writing failures mask the original smoke failure.
190
+ }
191
+ throw error;
192
+ }
141
193
  finally {
142
- await options.client.close();
194
+ try {
195
+ await options.client.close();
196
+ }
197
+ catch (closeError) {
198
+ if (!failed)
199
+ throw closeError;
200
+ }
143
201
  }
144
202
  }
145
203
  export function defaultAppServerArgs() {
@@ -197,3 +255,80 @@ function summarizeMethods(methods) {
197
255
  function writeJson(path, value) {
198
256
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
199
257
  }
258
+ function writeFailureReport(options, state) {
259
+ const blocker = classifyFailureBlocker(state.error);
260
+ const report = {
261
+ kind: "loo_live_control_smoke_failure",
262
+ ok: false,
263
+ proofReady: false,
264
+ generatedAt: options.now ?? new Date().toISOString(),
265
+ blocker,
266
+ command: {
267
+ action: "send",
268
+ actionClass: "codex_live_control_send"
269
+ },
270
+ target: {
271
+ source: state.target.source,
272
+ refClass: state.target.threadId ? "codex_thread" : "unknown",
273
+ ref: state.target.threadId ? `codex_thread:${state.target.threadId}` : null
274
+ },
275
+ dryRun: {
276
+ attempted: state.dryRun !== null,
277
+ approvalAuditId: state.dryRun?.approvalAuditId ?? null,
278
+ paramsHash: state.dryRun?.paramsHash ?? null,
279
+ messageHash: state.dryRun?.messageHash ?? null,
280
+ live: state.dryRun ? false : null
281
+ },
282
+ transport: {
283
+ class: "codex_app_server",
284
+ connection: "single_connection_sequence"
285
+ },
286
+ live: state.live,
287
+ postActionRefresh: {
288
+ ran: false
289
+ },
290
+ nextDiagnosticStep: nextDiagnosticStep(blocker),
291
+ rawPromptIncluded: false,
292
+ rawTranscriptIncluded: false,
293
+ rawSecretIncluded: false,
294
+ screenshotIncluded: false,
295
+ sqliteIncluded: false
296
+ };
297
+ writeJson(join(options.evidenceDir, "live-control-smoke-failure-report.json"), report);
298
+ }
299
+ function classifyFailureBlocker(error) {
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ if (/connect|handshake|spawn|ENOENT|codex.*not found|not connected/i.test(message)) {
302
+ return "codex_app_server_setup_failed";
303
+ }
304
+ if (/thread(?:\/resume| not found)|control sequence step failed:\s*thread\/resume/i.test(message)) {
305
+ return "same_connection_resume_load_diagnostics_required";
306
+ }
307
+ if (/approval request/i.test(message))
308
+ return "codex_approval_request_observed";
309
+ if (/server request/i.test(message))
310
+ return "codex_server_request_observed";
311
+ if (/turn id/i.test(message))
312
+ return "live_control_turn_id_missing";
313
+ if (/did not complete cleanly|timeout/i.test(message))
314
+ return "live_control_smoke_not_completed_cleanly";
315
+ return "live_control_smoke_failed";
316
+ }
317
+ function nextDiagnosticStep(blocker) {
318
+ switch (blocker) {
319
+ case "codex_app_server_setup_failed":
320
+ return "Check Codex app-server command availability, stdio handshake, and local transport setup before retrying live control.";
321
+ case "same_connection_resume_load_diagnostics_required":
322
+ return "Run same-connection resume/load diagnostics for the selected thread before retrying live control.";
323
+ case "codex_approval_request_observed":
324
+ return "Inspect why the harmless smoke triggered a Codex approval request; do not treat this as approved live-control proof.";
325
+ case "codex_server_request_observed":
326
+ return "Inspect unexpected server requests from the app-server smoke; do not retry live control until the request class is understood.";
327
+ case "live_control_turn_id_missing":
328
+ return "Inspect the app-server turn/start response shape and protocol drift before retrying.";
329
+ case "live_control_smoke_not_completed_cleanly":
330
+ return "Inspect turn completion notifications, status, and timeout behavior before retrying live control.";
331
+ default:
332
+ return "Inspect the public-safe failure report and route a focused issue before retrying live control.";
333
+ }
334
+ }