lossless-openclaw-orchestrator 1.2.2 → 1.2.4
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 +2 -2
- package/VISION.md +4 -4
- package/dist/packages/cli/src/release-preflight.js +59 -18
- package/dist/packages/core/src/index.js +30 -26
- package/docs/BETA_RELEASE_RUNBOOK.md +4 -4
- package/docs/CLAIM_AUDIT.md +5 -5
- package/docs/RELEASE_NOTES_1.2.3.md +84 -0
- package/docs/RELEASE_NOTES_1.2.4.md +92 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +9 -1
- package/packages/cli/src/release-preflight.ts +65 -16
- package/packages/core/src/index.ts +33 -26
- package/packages/openclaw-plugin/openclaw.plugin.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ safe action without rereading raw transcripts.
|
|
|
23
23
|
| Approval-gated boundaries | Dry-run Codex actions and verify matching audit ids before any live control. |
|
|
24
24
|
| OpenClaw/MCP tools | Use the same local-first recall and approval-bounded surfaces from agent workflows. |
|
|
25
25
|
|
|
26
|
-
[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.2.2.md) · [1.2.1 Notes](docs/RELEASE_NOTES_1.2.1.md) · [1.2.0 Notes](docs/RELEASE_NOTES_1.2.0.md) · [1.1.4 Notes](docs/RELEASE_NOTES_1.1.4.md) · [1.0 Notes](docs/RELEASE_NOTES_1.0.0.md) · [License](LICENSE)
|
|
26
|
+
[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.2.4.md) · [1.2.3 Notes](docs/RELEASE_NOTES_1.2.3.md) · [1.2.2 Notes](docs/RELEASE_NOTES_1.2.2.md) · [1.2.1 Notes](docs/RELEASE_NOTES_1.2.1.md) · [1.2.0 Notes](docs/RELEASE_NOTES_1.2.0.md) · [1.1.4 Notes](docs/RELEASE_NOTES_1.1.4.md) · [1.0 Notes](docs/RELEASE_NOTES_1.0.0.md) · [License](LICENSE)
|
|
27
27
|
|
|
28
28
|
## Why It Matters
|
|
29
29
|
|
|
@@ -325,7 +325,7 @@ The stable public product is Codex-first local orchestration: index, search,
|
|
|
325
325
|
describe, expand, prepared-state recall, OpenClaw/MCP tools, and
|
|
326
326
|
approval-gated dry-run/control boundaries.
|
|
327
327
|
|
|
328
|
-
The 1.2 prepared-state and summary-leaves lane is shipped in stable `1.2.
|
|
328
|
+
The 1.2 prepared-state and summary-leaves lane is shipped in stable `1.2.4`.
|
|
329
329
|
Current launch work is the M11 GA assurance sprint, tracked in GitHub and
|
|
330
330
|
summarized in [VISION.md](VISION.md). Keep sprint and agent-operator details
|
|
331
331
|
there, in [AGENTS.md](AGENTS.md), and in the packaged
|
package/VISION.md
CHANGED
|
@@ -12,11 +12,11 @@ The stable product should feel like a local orchestration cockpit: OpenClaw can
|
|
|
12
12
|
|
|
13
13
|
The current release-readiness lane is Milestone 11: LCO 1.2.0 GA Assurance And Global Launch Readiness. GitHub tracker [#478](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/478) and children [#479](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/479)-[#493](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/493) own implementation truth; this file owns the product and eval boundary.
|
|
14
14
|
|
|
15
|
-
The stable 1.0.0, 1.1.0, 1.1.1, 1.1.2, 1.1.3, 1.1.4, 1.2.0, 1.2.1, and 1.2.
|
|
15
|
+
The stable 1.0.0, 1.1.0, 1.1.1, 1.1.2, 1.1.3, 1.1.4, 1.2.0, 1.2.1, 1.2.2, and 1.2.3 packages have shipped on npm `latest` and their GitHub Releases are published. The 1.2.4 package is the current stable patch candidate; npm `latest` and GitHub Release finalization must be proven by release evidence before any global-ready claim treats it as 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 1.1 collaboration cockpit [#309](https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues/309) is completed proof for read-only collaboration summaries and execute-false next steps, not the current active child-work list.
|
|
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
|
|
|
19
|
-
The core Codex recall, M9 handoff paths, Codex Autonomy Cockpit, Eva Operating Picture P0 paths, 1.1 Desktop collaboration cockpit paths, 1.2 prepared-state and summary-leaf paths, release metadata, package/gateway setup proof, docs truth, release gates, public-safe scorecards
|
|
19
|
+
The core Codex recall, M9 handoff paths, Codex Autonomy Cockpit, Eva Operating Picture P0 paths, 1.1 Desktop collaboration cockpit paths, 1.2 prepared-state and summary-leaf paths, release metadata, package/gateway setup proof, docs truth, release gates, and public-safe scorecards are no longer the main product gap. The current gap is no-bug-left-behind launch assurance: prove the `1.2.4` stable candidate is published, installable from npm `@latest`, discoverable, public-safe, and honest for global users before broad rollout.
|
|
20
20
|
|
|
21
21
|
The current target is:
|
|
22
22
|
|
|
@@ -38,7 +38,7 @@ The current target is:
|
|
|
38
38
|
- Keep P1 source adapters, including Notion, support-control, Company Brain, Stripe, dashboard/export, and model summarization, behind separate adapters and proof gates; P0 tools report those sources as `not_configured` instead of fabricating summaries.
|
|
39
39
|
- Use the [source authority profile](docs/SOURCE_AUTHORITY_PROFILE.md) to distinguish "this source returned data" from "this source owns the current truth"; unavailable or cache-only sources must degrade claims to `unknown` or low confidence.
|
|
40
40
|
|
|
41
|
-
The sprint remains Codex-first, local-first, read-only-first, and public-safe by default. It does not claim full business truth, customer readiness, Claude Code parity, remote sync, generic GUI mutation, unattended desktop control, permission bypass, or enterprise/customer-ready security. npm `latest` promotion and GitHub Release creation
|
|
41
|
+
The sprint remains Codex-first, local-first, read-only-first, and public-safe by default. It does not claim full business truth, customer readiness, Claude Code parity, remote sync, generic GUI mutation, unattended desktop control, permission bypass, or enterprise/customer-ready security. npm `latest` promotion and GitHub Release creation for 1.2.4 are release gates, not assumptions. Desktop-visible classification and fallback readiness/status are proven by #307/#308; actual Codex GUI mutation remains excluded until a future action-bound proof gate records the exact backend, target, action hash, approval, and observation.
|
|
42
42
|
|
|
43
43
|
What 1.2 should let a local OpenClaw agent do next:
|
|
44
44
|
|
|
@@ -294,7 +294,7 @@ Release candidates follow [docs/BETA_RELEASE_RUNBOOK.md](docs/BETA_RELEASE_RUNBO
|
|
|
294
294
|
|
|
295
295
|
## Proof Boundary
|
|
296
296
|
|
|
297
|
-
Allowed stable 1.2.
|
|
297
|
+
Allowed stable 1.2.4 claim:
|
|
298
298
|
|
|
299
299
|
> Collaborate with local Codex sessions through OpenClaw using local indexing, prepared-state recall, bounded expansion, and approval-gated dry-run/control boundaries.
|
|
300
300
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import {
|
|
3
|
+
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { excludedClaimsForScope, liveControlExcludedDetail, normalizeReleaseClaimScope, releaseClaimScopeRequiresLiveControl, releaseClaimScopeRequiresWorkingAppRuntimeProof } from "./release-claim-scope.js";
|
|
5
5
|
import { validateWorkingAppRuntimeProof } from "./runtime-proof-gate.js";
|
|
6
|
+
const EVIDENCE_SCAN_MAX_DEPTH = 40;
|
|
6
7
|
const forbiddenClaims = [
|
|
7
8
|
"Full Claude Code parity",
|
|
8
9
|
"cloud sync",
|
|
@@ -68,14 +69,18 @@ export function runReleasePreflight(options = {}) {
|
|
|
68
69
|
const workingAppRuntimeProof = workingAppRuntimeProofRequired
|
|
69
70
|
? validateWorkingAppRuntimeProof(options.runtimeProofDir)
|
|
70
71
|
: null;
|
|
71
|
-
const
|
|
72
|
+
const evidenceScan = scanEvidenceArtifacts(options.evidenceDir);
|
|
73
|
+
const rawSessionArtifacts = evidenceScan.rawSessionArtifacts;
|
|
74
|
+
const evidenceScanDepthExceeded = evidenceScan.depthLimitExceeded;
|
|
72
75
|
const checks = {
|
|
73
76
|
packageJson: check(Boolean(!packageJsonRead.error && packageJson?.name && packageJson.version && packageJson.description?.match(/local Codex sessions/i)), packageJsonRead.error ?? "package metadata keeps Codex-first beta positioning"),
|
|
74
77
|
readme: check(Boolean(readme && readmePublicDocsRequiredPatterns.every((pattern) => pattern.test(readme))), "README includes public setup path, OpenClaw/MCP entrypoints, safety boundaries, and forbidden claims"),
|
|
75
78
|
openclawManifest: check(Boolean(!openclawManifestRead.error && openclawPackageMetadataOk && openclawManifest?.id === "lossless-openclaw-orchestrator" && openclawManifest.mcp?.command === "loo-mcp-server" && openclawManifest.mcp.transport === "stdio" && openclawManifest.tools?.prefix === "loo_" && openclawManifest.safety?.localOnlyByDefault === true), openclawManifestRead.error ?? "root OpenClaw manifest and package runtime entry are packageable and local-only by default"),
|
|
76
79
|
claimAudit: check(Boolean(claimAudit?.match(/Forbidden Beta Claims/i) && claimAudit.match(/approved_live_control_smoke_missing/i)), "claim audit records forbidden claims and the live-control blocker code"),
|
|
77
80
|
betaDemo: check(Boolean(betaDemo?.match(/100\+ local Codex sessions/i) && betaDemo.match(/does not run live control/i)), "demo workflow covers 100+ Codex sessions and dry-run-only control boundary"),
|
|
78
|
-
rawArtifacts: check(rawSessionArtifacts.length === 0
|
|
81
|
+
rawArtifacts: check(rawSessionArtifacts.length === 0 && evidenceScanDepthExceeded.length === 0, rawSessionArtifacts.length === 0 && evidenceScanDepthExceeded.length === 0
|
|
82
|
+
? "no raw session/private DB/screenshot artifacts found"
|
|
83
|
+
: "raw artifacts or unsafe evidence tree shape are present"),
|
|
79
84
|
liveControlSmoke: liveControlProof,
|
|
80
85
|
workingAppRuntimeProof: workingAppRuntimeProof
|
|
81
86
|
? check(workingAppRuntimeProof.ok, workingAppRuntimeProof.ok
|
|
@@ -88,6 +93,8 @@ export function runReleasePreflight(options = {}) {
|
|
|
88
93
|
.map(([key]) => `${key}_failed`);
|
|
89
94
|
if (rawSessionArtifacts.length > 0)
|
|
90
95
|
blockers.push("raw_session_artifacts_present");
|
|
96
|
+
if (evidenceScanDepthExceeded.length > 0)
|
|
97
|
+
blockers.push("evidence_scan_depth_exceeded");
|
|
91
98
|
if (liveControlRequired && !checks.liveControlSmoke?.ok)
|
|
92
99
|
blockers.push("approved_live_control_smoke_missing");
|
|
93
100
|
if (workingAppRuntimeProofRequired && workingAppRuntimeProof && !workingAppRuntimeProof.ok) {
|
|
@@ -105,7 +112,8 @@ export function runReleasePreflight(options = {}) {
|
|
|
105
112
|
checks,
|
|
106
113
|
blockers,
|
|
107
114
|
forbiddenClaims,
|
|
108
|
-
rawSessionArtifacts
|
|
115
|
+
rawSessionArtifacts,
|
|
116
|
+
evidenceScanDepthExceeded
|
|
109
117
|
};
|
|
110
118
|
if (report.artifactManifestPath) {
|
|
111
119
|
mkdirSync(options.evidenceDir, { recursive: true });
|
|
@@ -190,27 +198,60 @@ function findPackageRoot(start) {
|
|
|
190
198
|
cursor = parent;
|
|
191
199
|
}
|
|
192
200
|
}
|
|
193
|
-
function
|
|
194
|
-
if (!evidenceDir || !existsSync(evidenceDir))
|
|
195
|
-
return [];
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
function scanEvidenceArtifacts(evidenceDir) {
|
|
202
|
+
if (!evidenceDir || !existsSync(evidenceDir)) {
|
|
203
|
+
return { rawSessionArtifacts: [], depthLimitExceeded: [] };
|
|
204
|
+
}
|
|
205
|
+
const root = resolve(evidenceDir);
|
|
206
|
+
const rawSessionArtifacts = [];
|
|
207
|
+
const depthLimitExceeded = [];
|
|
208
|
+
const visit = (dir, depth) => {
|
|
209
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
210
|
+
const absolutePath = join(dir, entry.name);
|
|
211
|
+
const relativePath = normalizePackagePath(relative(root, absolutePath));
|
|
212
|
+
if (entry.isSymbolicLink())
|
|
213
|
+
continue;
|
|
214
|
+
if (entry.isDirectory()) {
|
|
215
|
+
if (depth >= EVIDENCE_SCAN_MAX_DEPTH) {
|
|
216
|
+
depthLimitExceeded.push(relativePath);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
visit(absolutePath, depth + 1);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!entry.isFile())
|
|
223
|
+
continue;
|
|
224
|
+
const artifact = rawArtifactForName(relativePath);
|
|
225
|
+
if (artifact)
|
|
226
|
+
rawSessionArtifacts.push(artifact);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
visit(root, 0);
|
|
230
|
+
return {
|
|
231
|
+
rawSessionArtifacts: rawSessionArtifacts
|
|
232
|
+
.filter((entry) => entry !== null)
|
|
233
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
234
|
+
depthLimitExceeded: depthLimitExceeded.sort((left, right) => left.localeCompare(right))
|
|
235
|
+
};
|
|
201
236
|
}
|
|
202
237
|
function rawArtifactForName(name) {
|
|
203
238
|
if (name === "release-preflight.json")
|
|
204
239
|
return null;
|
|
205
|
-
const
|
|
240
|
+
const normalizedName = normalizePackagePath(name);
|
|
241
|
+
const extension = extname(normalizedName).toLowerCase();
|
|
242
|
+
const lowerName = normalizedName.toLowerCase();
|
|
206
243
|
if (extension === ".jsonl")
|
|
207
|
-
return { name:
|
|
208
|
-
if (extension === ".sqlite" || extension === ".sqlite3" || extension === ".db"
|
|
209
|
-
|
|
244
|
+
return { name: normalizedName, reason: "raw_codex_jsonl" };
|
|
245
|
+
if (extension === ".sqlite" || extension === ".sqlite3" || extension === ".db"
|
|
246
|
+
|| lowerName.endsWith(".sqlite-wal") || lowerName.endsWith(".sqlite-shm")
|
|
247
|
+
|| lowerName.endsWith(".sqlite3-wal") || lowerName.endsWith(".sqlite3-shm")
|
|
248
|
+
|| lowerName.endsWith(".db-wal") || lowerName.endsWith(".db-shm")) {
|
|
249
|
+
return { name: normalizedName, reason: "sqlite_database" };
|
|
250
|
+
}
|
|
210
251
|
if (extension === ".png" || extension === ".jpg" || extension === ".jpeg" || extension === ".heic" || extension === ".webp")
|
|
211
|
-
return { name:
|
|
252
|
+
return { name: normalizedName, reason: "screenshot_or_image" };
|
|
212
253
|
if (extension === ".mov" || extension === ".mp4" || extension === ".webm")
|
|
213
|
-
return { name:
|
|
254
|
+
return { name: normalizedName, reason: "video_capture" };
|
|
214
255
|
return null;
|
|
215
256
|
}
|
|
216
257
|
function validateApprovedLiveControlProof(path) {
|
|
@@ -4004,13 +4004,13 @@ function formatClaudeSessionInventoryMetadata(description) {
|
|
|
4004
4004
|
return [
|
|
4005
4005
|
`Claude session ID: ${description.sessionId}`,
|
|
4006
4006
|
`Ref: ${description.sourceRef}`,
|
|
4007
|
-
description.title ? `Title: ${description.title}` : null,
|
|
4008
|
-
description.project ? `Project: ${description.project}` : null,
|
|
4009
|
-
description.workspaceHint ? `Workspace: ${description.workspaceHint}` : null,
|
|
4007
|
+
description.title ? `Title: ${publicSafeText(description.title, 500)}` : null,
|
|
4008
|
+
description.project ? `Project: ${publicSafeText(description.project, 500)}` : null,
|
|
4009
|
+
description.workspaceHint ? `Workspace: ${publicSafeText(description.workspaceHint, 500)}` : null,
|
|
4010
4010
|
description.status ? `Status: ${description.status}` : null,
|
|
4011
4011
|
description.updatedAt ? `Updated: ${description.updatedAt}` : null,
|
|
4012
|
-
`Source
|
|
4013
|
-
description.sourceRefs.length ? `Source refs: ${description.sourceRefs.join(", ")}` : null,
|
|
4012
|
+
`Source ref: ${publicSourcePathRef(description.sourcePath)}`,
|
|
4013
|
+
description.sourceRefs.length ? `Source refs: ${description.sourceRefs.map((ref) => publicSafeText(ref, 180)).join(", ")}` : null,
|
|
4014
4014
|
"Proof boundary: read-only Claude metadata fixture inventory only; no private transcript content, live control, GUI mutation, parity, or cloud sync proof."
|
|
4015
4015
|
].filter(Boolean).join("\n");
|
|
4016
4016
|
}
|
|
@@ -8623,15 +8623,15 @@ export function expandSession(db, options) {
|
|
|
8623
8623
|
`Thread: ${description.title ?? description.threadId}`,
|
|
8624
8624
|
`Ref: ${description.sourceRef}`,
|
|
8625
8625
|
`ID: ${description.threadId}`,
|
|
8626
|
-
description.cwd ? `CWD: ${description.cwd}` : null,
|
|
8626
|
+
description.cwd ? `CWD: ${publicSafeText(description.cwd, 500)}` : null,
|
|
8627
8627
|
description.branch ? `Branch: ${description.branch}` : null,
|
|
8628
8628
|
description.gitSha ? `Git SHA: ${description.gitSha}` : null,
|
|
8629
|
-
description.summary ? `Summary: ${description.summary}` : null,
|
|
8629
|
+
description.summary ? `Summary: ${publicSafeText(description.summary, 2000)}` : null,
|
|
8630
8630
|
formatSessionMetadata(description.metadata),
|
|
8631
8631
|
`Plans: ${description.planCount}`,
|
|
8632
8632
|
`Touched files: ${description.touchedFiles.length}`,
|
|
8633
8633
|
`Tool calls: ${description.toolCallCount}`,
|
|
8634
|
-
`Source
|
|
8634
|
+
`Source ref: ${publicSourcePathRef(description.sourcePath)}`
|
|
8635
8635
|
].filter(Boolean).join("\n");
|
|
8636
8636
|
return {
|
|
8637
8637
|
sourceKind: "codex_thread",
|
|
@@ -8645,13 +8645,13 @@ export function expandSession(db, options) {
|
|
|
8645
8645
|
const text = [
|
|
8646
8646
|
`Thread: ${description.title ?? description.threadId}`,
|
|
8647
8647
|
`ID: ${description.threadId}`,
|
|
8648
|
-
description.cwd ? `CWD: ${description.cwd}` : null,
|
|
8648
|
+
description.cwd ? `CWD: ${publicSafeText(description.cwd, 500)}` : null,
|
|
8649
8649
|
description.branch ? `Branch: ${description.branch}` : null,
|
|
8650
8650
|
description.gitSha ? `Git SHA: ${description.gitSha}` : null,
|
|
8651
|
-
description.summary ? `Summary: ${description.summary}` : null,
|
|
8652
|
-
description.finalMessage ? `Final message: ${
|
|
8651
|
+
description.summary ? `Summary: ${publicSafeText(description.summary, profile.name === "evidence" ? 3200 : 1600)}` : null,
|
|
8652
|
+
description.finalMessage ? `Final message: ${publicSafeText(description.finalMessage, profile.name === "evidence" ? 3200 : 900)}` : null,
|
|
8653
8653
|
description.touchedFiles.length ? `Touched files:\n${formatTouchedFiles(description.touchedFiles, profile.name === "evidence" ? 50 : 12, profile.name === "evidence" ? 3200 : 900)}` : null,
|
|
8654
|
-
plans.length ? `Plans:\n${plans.map((plan) =>
|
|
8654
|
+
plans.length ? `Plans:\n${plans.map((plan) => publicSafeText(plan, profile.name === "evidence" ? 3200 : 1200)).join("\n\n")}` : null
|
|
8655
8655
|
].filter(Boolean).join("\n\n");
|
|
8656
8656
|
return {
|
|
8657
8657
|
sourceKind: "codex_thread",
|
|
@@ -8666,7 +8666,7 @@ function formatTouchedFiles(files, limit, maxChars) {
|
|
|
8666
8666
|
const perPathLimit = maxChars > 1000 ? 180 : 120;
|
|
8667
8667
|
const visible = [];
|
|
8668
8668
|
for (const file of files.slice(0, limit)) {
|
|
8669
|
-
const next = `- ${
|
|
8669
|
+
const next = `- ${publicSafeText(file, perPathLimit)}`;
|
|
8670
8670
|
const hiddenIfAccepted = files.length - (visible.length + 1);
|
|
8671
8671
|
const markerIfAccepted = hiddenIfAccepted > 0 ? `- ... ${hiddenIfAccepted} more touched files omitted` : null;
|
|
8672
8672
|
const visibleMaxChars = markerIfAccepted ? Math.max(0, maxChars - markerIfAccepted.length - 1) : maxChars;
|
|
@@ -8760,7 +8760,7 @@ export function expandRecallRef(db, options) {
|
|
|
8760
8760
|
const metadata = formatClaudeSessionInventoryMetadata(description);
|
|
8761
8761
|
const text = profile.name === "metadata"
|
|
8762
8762
|
? metadata
|
|
8763
|
-
: truncateByApproxTokens(`${metadata}\n\nSafe summary:\n${description.summary ?? ""}`, profile.tokenBudget);
|
|
8763
|
+
: truncateByApproxTokens(`${metadata}\n\nSafe summary:\n${publicSafeText(description.summary ?? "", profile.tokenBudget * 6)}`, profile.tokenBudget);
|
|
8764
8764
|
return {
|
|
8765
8765
|
sourceKind: "claude_session",
|
|
8766
8766
|
sourceRef: description.sourceRef,
|
|
@@ -8777,18 +8777,18 @@ export function expandRecallRef(db, options) {
|
|
|
8777
8777
|
const metadata = [
|
|
8778
8778
|
`Summary ID: ${summary.summaryId}`,
|
|
8779
8779
|
`Ref: ${lcmSummaryRef(summary.sourcePath, summary.summaryId)}`,
|
|
8780
|
-
`Conversation: ${summary.conversationTitle ?? summary.conversationId}`,
|
|
8780
|
+
`Conversation: ${publicSafeText(summary.conversationTitle ?? String(summary.conversationId), 500)}`,
|
|
8781
8781
|
`Conversation ID: ${summary.conversationId}`,
|
|
8782
8782
|
summary.kind ? `Kind: ${summary.kind}` : null,
|
|
8783
8783
|
summary.depth !== null ? `Depth: ${summary.depth}` : null,
|
|
8784
8784
|
summary.tokenCount !== null ? `Token count: ${summary.tokenCount}` : null,
|
|
8785
|
-
summary.model ? `Model: ${summary.model}` : null,
|
|
8785
|
+
summary.model ? `Model: ${publicSafeText(summary.model, 120)}` : null,
|
|
8786
8786
|
summary.updatedAt ? `Updated: ${summary.updatedAt}` : null,
|
|
8787
|
-
`Source
|
|
8787
|
+
`Source ref: ${publicSourcePathRef(summary.sourcePath)}`
|
|
8788
8788
|
].filter(Boolean).join("\n");
|
|
8789
8789
|
const text = profile.name === "metadata"
|
|
8790
8790
|
? metadata
|
|
8791
|
-
: truncateByApproxTokens(`${metadata}\n\nContent:\n${summary.content}`, profile.tokenBudget);
|
|
8791
|
+
: truncateByApproxTokens(`${metadata}\n\nContent:\n${publicSafeText(summary.content, profile.tokenBudget * 6)}`, profile.tokenBudget);
|
|
8792
8792
|
return {
|
|
8793
8793
|
sourceKind: "lcm_summary",
|
|
8794
8794
|
sourceRef: lcmSummaryRef(summary.sourcePath, summary.summaryId),
|
|
@@ -9055,7 +9055,7 @@ function lcmSummaryRecord(path, row) {
|
|
|
9055
9055
|
conversationTitle: nullableString(row.conversationTitle),
|
|
9056
9056
|
kind: nullableString(row.kind),
|
|
9057
9057
|
depth: row.depth === null || row.depth === undefined ? null : Number(row.depth),
|
|
9058
|
-
content:
|
|
9058
|
+
content: redactPublicSafeString(String(row.content ?? "")),
|
|
9059
9059
|
tokenCount: row.tokenCount === null || row.tokenCount === undefined ? null : Number(row.tokenCount),
|
|
9060
9060
|
createdAt: nullableString(row.createdAt),
|
|
9061
9061
|
updatedAt: nullableString(row.updatedAt),
|
|
@@ -9160,13 +9160,17 @@ function publicSourcePathRef(sourcePath) {
|
|
|
9160
9160
|
return `codex_source:${stableId(sourcePath).slice(0, 16)}`;
|
|
9161
9161
|
}
|
|
9162
9162
|
function publicSafeText(value, maxChars = 500) {
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9163
|
+
return truncate(redactPublicSafeString(value), maxChars);
|
|
9164
|
+
}
|
|
9165
|
+
function redactPublicSafeString(value) {
|
|
9166
|
+
const localPathRootPattern = "(?:\\/Volumes\\/|\\/(?:Users|home|root)\\/|\\/(?:private\\/)?(?:tmp|var)\\/|~\\/|(?<![A-Za-z])[A-Za-z]:[\\\\/])";
|
|
9167
|
+
const structuredLabelPattern = "[A-Za-z][A-Za-z0-9 _-]{0,32}:";
|
|
9168
|
+
const relativePathStartPattern = "(?:\\.{1,2}\\/|[A-Za-z0-9_.-]+\\/)";
|
|
9169
|
+
const omissionMarkerPattern = "\\+\\d+\\s+more\\b";
|
|
9170
|
+
const localPathTerminatorPattern = `(?=$|[\\r\\n"'\\\`)\\]}]|\\s+(?:${localPathRootPattern}|${relativePathStartPattern}|${omissionMarkerPattern}|${structuredLabelPattern}))`;
|
|
9171
|
+
const localPathPattern = new RegExp(`${localPathRootPattern}(?:(?!${localPathTerminatorPattern}).)+`, "g");
|
|
9172
|
+
const pathRedacted = value.replace(localPathPattern, "<redacted-path>");
|
|
9173
|
+
return redactSafeString(pathRedacted).replace(localPathPattern, "<redacted-path>");
|
|
9170
9174
|
}
|
|
9171
9175
|
function publicSafeSearchText(value, maxChars = 500) {
|
|
9172
9176
|
return publicSafeText(value, maxChars);
|
|
@@ -384,10 +384,10 @@ Record `npm dist-tag ls lossless-openclaw-orchestrator` in the release evidence
|
|
|
384
384
|
after every npm publication. Stable releases publish with
|
|
385
385
|
`npm publish --tag latest`; public betas publish with `npm publish --tag beta`;
|
|
386
386
|
release candidates publish with `npm publish --tag next`. The stable channel
|
|
387
|
-
|
|
388
|
-
separate stable-promotion gate proves the exact candidate. Keep beta and
|
|
389
|
-
prereleases on prerelease tags. Do not publish a fake stable package just
|
|
390
|
-
move a dist-tag. Release candidates must publish with `npm publish --tag next`;
|
|
387
|
+
target for this package version is `1.2.4`; npm `latest` must move only after
|
|
388
|
+
the separate stable-promotion gate proves the exact candidate. Keep beta and
|
|
389
|
+
other prereleases on prerelease tags. Do not publish a fake stable package just
|
|
390
|
+
to move a dist-tag. Release candidates must publish with `npm publish --tag next`;
|
|
391
391
|
RC branches also carry `package.json` `publishConfig.tag` set to `next` as a
|
|
392
392
|
fail-closed guard against accidental untagged publication. Stable branches carry
|
|
393
393
|
`package.json` `publishConfig.tag` set to `latest`. Do not run untagged
|
package/docs/CLAIM_AUDIT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Public Claim Audit
|
|
2
2
|
|
|
3
|
-
## Allowed Stable 1.2.
|
|
3
|
+
## Allowed Stable 1.2.4 Claim
|
|
4
4
|
|
|
5
5
|
Collaborate with local Codex sessions through OpenClaw using local indexing, prepared-state recall, bounded expansion, and approval-gated dry-run/control boundaries.
|
|
6
6
|
|
|
@@ -75,10 +75,10 @@ blockers instead of allowing a working-app claim.
|
|
|
75
75
|
|
|
76
76
|
Install stable releases through the `latest` dist-tag, public betas through the
|
|
77
77
|
`beta` dist-tag, and release candidates through `next`. The stable channel
|
|
78
|
-
|
|
79
|
-
separate stable-promotion gate proves the exact candidate. Keep beta and
|
|
80
|
-
prereleases on prerelease tags. Do not publish a fake stable package just
|
|
81
|
-
move a dist-tag.
|
|
78
|
+
target for this package version is `1.2.4`; npm `latest` must move only after
|
|
79
|
+
the separate stable-promotion gate proves the exact candidate. Keep beta and
|
|
80
|
+
other prereleases on prerelease tags. Do not publish a fake stable package just
|
|
81
|
+
to move a dist-tag.
|
|
82
82
|
|
|
83
83
|
## Release Checklist
|
|
84
84
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Release Notes 1.2.3
|
|
2
|
+
|
|
3
|
+
`1.2.3` is the GA-assurance hygiene patch for the LCO 1.2 stable train and Codex-first local orchestration. It fixes the recursive release-evidence scan gap found during M11 adversarial review and keeps the 1.2 claim boundary unchanged.
|
|
4
|
+
|
|
5
|
+
This release is focused on local Codex sessions. Prepared state remains an advisory local derived cache, not source authority for PR, CI, release, runtime, customer, or business truth.
|
|
6
|
+
|
|
7
|
+
## What Changed
|
|
8
|
+
|
|
9
|
+
- Fixes `loo release preflight` raw-artifact detection so it scans nested evidence folders instead of only immediate files in `--evidence-dir`.
|
|
10
|
+
- Adds recursive detection for SQLite sidecars such as `.sqlite-wal`, `.sqlite-shm`, `.sqlite3-wal`, `.sqlite3-shm`, `.db-wal`, and `.db-shm`.
|
|
11
|
+
- Skips symlinked evidence entries and reports `evidence_scan_depth_exceeded`
|
|
12
|
+
when an evidence tree is too deep to scan deterministically.
|
|
13
|
+
- Keeps raw artifact findings public-safe by reporting relative evidence names and blocker reasons, not raw artifact contents.
|
|
14
|
+
- Sanitizes the M11 final evidence packet by removing generated clean-profile runtime HOME/prefix artifacts while preserving public-safe summary reports.
|
|
15
|
+
- Carries forward the 1.2.2 readiness-smoke semantics clarification: `ok` / `packagePathOk` are package-path claims, while `publishedSmokeReady` is the clean-profile gateway-ready claim.
|
|
16
|
+
- Carries forward `--gateway-ready-strict` so operators and CI can explicitly fail when a fresh published profile still needs gateway credentials or device setup.
|
|
17
|
+
- Carries forward the hardened `loo release general-readiness` proof-boundary label so an unexpected package version string cannot inject uncontrolled text into the public proof boundary.
|
|
18
|
+
- Carries forward the frozen `readinessSemantics` metadata object emitted by `published-smoke`, keeping machine-readable exit semantics stable for release scripts and audits.
|
|
19
|
+
- Carries forward the 1.2.1 facade smoke fix for resume dry-run proof, including the distinction between resume packets and message-carrying send/steer packets.
|
|
20
|
+
- Carries forward the #160 desktop proof-action release boundary:
|
|
21
|
+
`loo_desktop_proof_action` / `loo desktop proof-action` validates the
|
|
22
|
+
CUA Driver TextEdit scratch proof gate and does not prove generic GUI
|
|
23
|
+
mutation.
|
|
24
|
+
The proof action still requires exact backend, target app, target window, action hash, approval ref, permission state, scratch file path, and `execute: true` before the backend can run.
|
|
25
|
+
Generic gateway invocation without exact proof args fails closed. The desktop
|
|
26
|
+
proof action keeps the same proof boundary as beta.35.
|
|
27
|
+
- Carries forward strict OpenClaw gateway result handling: plugin output with
|
|
28
|
+
`output.details.ok: false` is reported as
|
|
29
|
+
`openclaw_tool_result_not_ok:<tool>` rather than successful tool proof.
|
|
30
|
+
|
|
31
|
+
## Current Claim Scope
|
|
32
|
+
|
|
33
|
+
Allowed stable claim:
|
|
34
|
+
|
|
35
|
+
> Collaborate with local Codex sessions through OpenClaw using local indexing,
|
|
36
|
+
> prepared-state recall, bounded expansion, and approval-gated dry-run/control
|
|
37
|
+
> boundaries.
|
|
38
|
+
|
|
39
|
+
The stable channel means the scoped prepared-state and cockpit-management
|
|
40
|
+
surface is ready as a public local-Codex release. It does not broaden the
|
|
41
|
+
control, GUI, Claude, customer, or enterprise-security proof boundary.
|
|
42
|
+
|
|
43
|
+
## Release Gate Notes
|
|
44
|
+
|
|
45
|
+
- Parent 1.2 tracker: #405.
|
|
46
|
+
- GA assurance tracker: #478.
|
|
47
|
+
- Hygiene patch issue: #506.
|
|
48
|
+
- Previous patch-release issue: #503.
|
|
49
|
+
- Readiness-semantics issue: #494.
|
|
50
|
+
- Patch PRs: #499, #505, and the 1.2.3 release PR.
|
|
51
|
+
- Baseline stable release: `v1.2.2`.
|
|
52
|
+
- Candidate package: `lossless-openclaw-orchestrator@1.2.3`.
|
|
53
|
+
- Expected npm dist-tag: `latest`.
|
|
54
|
+
- Expected git tag: `v1.2.3`.
|
|
55
|
+
- Required stable gates: focused release-smoke tests, release claim audit,
|
|
56
|
+
build/typecheck, `npm pack --dry-run`, release bundle/status checks, GitHub
|
|
57
|
+
CI, CodeQL, current-head review threads clear, npm publish to `latest`,
|
|
58
|
+
GitHub Release, and post-publish finalization status.
|
|
59
|
+
- Bundle/status/finalization dry-run checks do not publish to npm and will not create a GitHub Release.
|
|
60
|
+
- Working-app status example:
|
|
61
|
+
`loo release status --claim-scope codex-working-app-proof --runtime-proof-dir <path> --approved-live-control-evidence <path> --npm-publish-approval-evidence <path> --github-release-approval-evidence <path> --candidate-sha <sha> --github-ci-evidence <path> --codeql-evidence <path> --evidence-dir <path> --strict`
|
|
62
|
+
- Reduced-scope status example:
|
|
63
|
+
`loo release status --claim-scope codex-read-search-expand-dry-run --npm-publish-approval-evidence <path> --github-release-approval-evidence <path> --candidate-sha <sha> --github-ci-evidence <path> --codeql-evidence <path> --evidence-dir <path> --strict`
|
|
64
|
+
- `approved_live_control_smoke_missing` remains the expected blocker when a
|
|
65
|
+
working-app or live-control claim is attempted without approved live-control
|
|
66
|
+
smoke evidence for the exact candidate SHA.
|
|
67
|
+
|
|
68
|
+
## Explicit Non-Claims
|
|
69
|
+
|
|
70
|
+
No new live Codex control smoke is run by this release.
|
|
71
|
+
It does not run generic GUI mutation and does not run Codex GUI mutation.
|
|
72
|
+
No automatic gateway authorization.
|
|
73
|
+
No broad gateway scope approval. No prompt typing. No clicking. No arbitrary app control.
|
|
74
|
+
No screenshots or videos are part of the public release evidence.
|
|
75
|
+
Claude Code remains an adapter stub, not an adapter-equivalence claim.
|
|
76
|
+
No true Codex compaction-summary capture.
|
|
77
|
+
No raw model compaction by default and no default model access to raw transcript
|
|
78
|
+
or current `safe_text`.
|
|
79
|
+
No raw transcript upload and no OpenClaw LCM merge.
|
|
80
|
+
No source-store mutation.
|
|
81
|
+
No Notion, support-control, Stripe, or Company Brain P1 adapter proof.
|
|
82
|
+
No cloud sync.
|
|
83
|
+
No unattended desktop takeover.
|
|
84
|
+
No release-grade enterprise security or customer-ready security claim.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Release Notes 1.2.4
|
|
2
|
+
|
|
3
|
+
`1.2.4` is a GA-assurance privacy hygiene patch for the LCO 1.2 stable train.
|
|
4
|
+
It keeps the 1.2 claim boundary unchanged and fixes a public-safe rendering gap
|
|
5
|
+
found during the M11 launch packet pass.
|
|
6
|
+
|
|
7
|
+
## What Changed
|
|
8
|
+
|
|
9
|
+
- Redacts local absolute paths from `loo expand-query`, `loo expand-ref`, and
|
|
10
|
+
`expandSession` human-readable expansion text.
|
|
11
|
+
- Keeps relative project paths visible where they are already public-safe, while
|
|
12
|
+
replacing local `/Volumes/...`, `/Users/...`, `.codex`, and tmp-style paths
|
|
13
|
+
with `<redacted-path>`.
|
|
14
|
+
- Keeps exact touched-file metadata in the local index for matching and recall;
|
|
15
|
+
only the rendered expansion text is sanitized.
|
|
16
|
+
- Updates bounded expansion snapshots and regression tests so future releases
|
|
17
|
+
fail if expansion evidence leaks local absolute paths.
|
|
18
|
+
- Carries forward the 1.2.3 recursive evidence scanner hardening for nested raw
|
|
19
|
+
artifacts, SQLite sidecars, symlinked evidence entries, and deep evidence
|
|
20
|
+
trees.
|
|
21
|
+
- Clarifies launch-truth wording from `1.2.0`/`1.2.3` to the current `1.2.4`
|
|
22
|
+
stable patch line.
|
|
23
|
+
|
|
24
|
+
## Current Claim Scope
|
|
25
|
+
|
|
26
|
+
Claim scope: `codex-read-search-expand-dry-run`.
|
|
27
|
+
|
|
28
|
+
Allowed stable claim:
|
|
29
|
+
|
|
30
|
+
> Collaborate with local Codex sessions through OpenClaw using local indexing,
|
|
31
|
+
> prepared-state recall, bounded expansion, and approval-gated dry-run/control
|
|
32
|
+
> boundaries.
|
|
33
|
+
|
|
34
|
+
When finalization evidence proves this candidate on the stable channel, the
|
|
35
|
+
scoped prepared-state and cockpit-management surface is ready as a public
|
|
36
|
+
local-Codex release. It does not broaden the control, GUI, Claude, customer, or
|
|
37
|
+
enterprise-security proof boundary.
|
|
38
|
+
|
|
39
|
+
This remains a Codex-first local orchestration release.
|
|
40
|
+
|
|
41
|
+
## Carried-Forward Desktop Proof Boundary
|
|
42
|
+
|
|
43
|
+
The Desktop proof-action hardening from #160 is still included. The public tool
|
|
44
|
+
surface includes `loo_desktop_proof_action` and the CLI command
|
|
45
|
+
`loo desktop proof-action`, but they remain bounded to the same proof boundary as beta.35: a CUA Driver TextEdit scratch proof only. The exact tuple is:
|
|
46
|
+
exact backend, target app, target window, action hash, approval ref, permission state, scratch file path, and `execute: true`.
|
|
47
|
+
|
|
48
|
+
A generic gateway invocation without exact proof args fails closed. The expected
|
|
49
|
+
OpenClaw failure shape is `openclaw_tool_result_not_ok:<tool>` with
|
|
50
|
+
`output.details.ok: false`; it is not proof of generic GUI mutation.
|
|
51
|
+
|
|
52
|
+
## Release Gate Notes
|
|
53
|
+
|
|
54
|
+
- Parent 1.2 tracker: #405.
|
|
55
|
+
- GA assurance tracker: #478.
|
|
56
|
+
- Path-redaction issue: #508.
|
|
57
|
+
- Hygiene patch issue: #506.
|
|
58
|
+
- Baseline stable release: `v1.2.3`.
|
|
59
|
+
- Candidate package: `lossless-openclaw-orchestrator@1.2.4`.
|
|
60
|
+
- Expected npm dist-tag: `latest`.
|
|
61
|
+
- Expected git tag: `v1.2.4`.
|
|
62
|
+
- Example reduced-scope release preflight:
|
|
63
|
+
`loo release preflight --claim-scope codex-read-search-expand-dry-run --evidence-dir <public-safe-evidence> --strict`.
|
|
64
|
+
- Example release status with all required non-GUI approvals:
|
|
65
|
+
`loo release status --candidate-sha <sha> --github-ci-evidence <ci.json> --codeql-evidence <codeql.json> --approved-live-control-evidence <live-control.json> --npm-publish-approval-evidence <npm-approval.json> --github-release-approval-evidence <github-release-approval.json> --strict`.
|
|
66
|
+
- Reduced-scope release status may keep `approved_live_control_smoke_missing`
|
|
67
|
+
only when `--claim-scope codex-read-search-expand-dry-run` is explicit and
|
|
68
|
+
live Codex control is recorded as excluded.
|
|
69
|
+
- Required stable gates: focused path-redaction tests, full `npm run check`,
|
|
70
|
+
release preflight/bundle/status, `npm pack --dry-run`, GitHub CI, CodeQL,
|
|
71
|
+
review threads clear, npm publish to `latest`, GitHub Release, post-publish
|
|
72
|
+
finalization status, fresh npm install, OpenClaw dogfood/tool-smoke,
|
|
73
|
+
scenario/scorecard sweeps, and privacy scan.
|
|
74
|
+
- `loo release bundle` prepares public-safe local artifacts; it does not publish to npm and does not create a GitHub Release.
|
|
75
|
+
|
|
76
|
+
## Explicit Non-Claims
|
|
77
|
+
|
|
78
|
+
No new live Codex control smoke is run by this release.
|
|
79
|
+
It does not run generic GUI mutation and does not run Codex GUI mutation.
|
|
80
|
+
No automatic gateway authorization.
|
|
81
|
+
No broad gateway scope approval. No prompt typing. No clicking. No arbitrary app control.
|
|
82
|
+
No screenshots or videos are part of the public release evidence.
|
|
83
|
+
Claude Code remains an adapter stub, not an adapter-equivalence claim.
|
|
84
|
+
No true Codex compaction-summary capture.
|
|
85
|
+
No raw model compaction by default and no default model access to raw transcript
|
|
86
|
+
or current `safe_text`.
|
|
87
|
+
No raw transcript upload and no OpenClaw LCM merge.
|
|
88
|
+
No source-store mutation.
|
|
89
|
+
No Notion, support-control, Stripe, or Company Brain P1 adapter proof.
|
|
90
|
+
No cloud sync.
|
|
91
|
+
No unattended desktop takeover.
|
|
92
|
+
No release-grade enterprise security or customer-ready security claim.
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "lossless-openclaw-orchestrator",
|
|
3
3
|
"name": "Lossless OpenClaw Orchestrator",
|
|
4
4
|
"description": "Collaborate with local Codex sessions through OpenClaw using local indexing, prepared-state recall, bounded expansion, and approval-gated dry-run/control boundaries.",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.4",
|
|
6
6
|
"kind": "tool",
|
|
7
7
|
"tools": {
|
|
8
8
|
"prefix": "loo_"
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lossless-openclaw-orchestrator",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Index, search, and prepare local Codex sessions for OpenClaw with approval-gated dry-run/control boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
7
7
|
"private": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/100yenadmin/Lossless-Codex-Orchestrator-LCO/issues"
|
|
15
|
+
},
|
|
8
16
|
"publishConfig": {
|
|
9
17
|
"tag": "latest"
|
|
10
18
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import {
|
|
3
|
+
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
excludedClaimsForScope,
|
|
6
6
|
liveControlExcludedDetail,
|
|
@@ -39,6 +39,7 @@ export type ReleasePreflightReport = {
|
|
|
39
39
|
blockers: string[];
|
|
40
40
|
forbiddenClaims: string[];
|
|
41
41
|
rawSessionArtifacts: RawSessionArtifact[];
|
|
42
|
+
evidenceScanDepthExceeded: string[];
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
type ApprovedLiveControlSmokeProof = {
|
|
@@ -57,11 +58,18 @@ type RawSessionArtifact = {
|
|
|
57
58
|
reason: "raw_codex_jsonl" | "sqlite_database" | "screenshot_or_image" | "video_capture";
|
|
58
59
|
};
|
|
59
60
|
|
|
61
|
+
type EvidenceScanResult = {
|
|
62
|
+
rawSessionArtifacts: RawSessionArtifact[];
|
|
63
|
+
depthLimitExceeded: string[];
|
|
64
|
+
};
|
|
65
|
+
|
|
60
66
|
type JsonReadResult = {
|
|
61
67
|
value: unknown | null;
|
|
62
68
|
error: string | null;
|
|
63
69
|
};
|
|
64
70
|
|
|
71
|
+
const EVIDENCE_SCAN_MAX_DEPTH = 40;
|
|
72
|
+
|
|
65
73
|
const forbiddenClaims = [
|
|
66
74
|
"Full Claude Code parity",
|
|
67
75
|
"cloud sync",
|
|
@@ -148,14 +156,21 @@ export function runReleasePreflight(options: ReleasePreflightOptions = {}): Rele
|
|
|
148
156
|
const workingAppRuntimeProof = workingAppRuntimeProofRequired
|
|
149
157
|
? validateWorkingAppRuntimeProof(options.runtimeProofDir)
|
|
150
158
|
: null;
|
|
151
|
-
const
|
|
159
|
+
const evidenceScan = scanEvidenceArtifacts(options.evidenceDir);
|
|
160
|
+
const rawSessionArtifacts = evidenceScan.rawSessionArtifacts;
|
|
161
|
+
const evidenceScanDepthExceeded = evidenceScan.depthLimitExceeded;
|
|
152
162
|
const checks: Record<string, ReleasePreflightCheck> = {
|
|
153
163
|
packageJson: check(Boolean(!packageJsonRead.error && packageJson?.name && packageJson.version && packageJson.description?.match(/local Codex sessions/i)), packageJsonRead.error ?? "package metadata keeps Codex-first beta positioning"),
|
|
154
164
|
readme: check(Boolean(readme && readmePublicDocsRequiredPatterns.every((pattern) => pattern.test(readme))), "README includes public setup path, OpenClaw/MCP entrypoints, safety boundaries, and forbidden claims"),
|
|
155
165
|
openclawManifest: check(Boolean(!openclawManifestRead.error && openclawPackageMetadataOk && openclawManifest?.id === "lossless-openclaw-orchestrator" && openclawManifest.mcp?.command === "loo-mcp-server" && openclawManifest.mcp.transport === "stdio" && openclawManifest.tools?.prefix === "loo_" && openclawManifest.safety?.localOnlyByDefault === true), openclawManifestRead.error ?? "root OpenClaw manifest and package runtime entry are packageable and local-only by default"),
|
|
156
166
|
claimAudit: check(Boolean(claimAudit?.match(/Forbidden Beta Claims/i) && claimAudit.match(/approved_live_control_smoke_missing/i)), "claim audit records forbidden claims and the live-control blocker code"),
|
|
157
167
|
betaDemo: check(Boolean(betaDemo?.match(/100\+ local Codex sessions/i) && betaDemo.match(/does not run live control/i)), "demo workflow covers 100+ Codex sessions and dry-run-only control boundary"),
|
|
158
|
-
rawArtifacts: check(
|
|
168
|
+
rawArtifacts: check(
|
|
169
|
+
rawSessionArtifacts.length === 0 && evidenceScanDepthExceeded.length === 0,
|
|
170
|
+
rawSessionArtifacts.length === 0 && evidenceScanDepthExceeded.length === 0
|
|
171
|
+
? "no raw session/private DB/screenshot artifacts found"
|
|
172
|
+
: "raw artifacts or unsafe evidence tree shape are present"
|
|
173
|
+
),
|
|
159
174
|
liveControlSmoke: liveControlProof,
|
|
160
175
|
workingAppRuntimeProof: workingAppRuntimeProof
|
|
161
176
|
? check(
|
|
@@ -171,6 +186,7 @@ export function runReleasePreflight(options: ReleasePreflightOptions = {}): Rele
|
|
|
171
186
|
.filter(([key, value]) => !value.ok && key !== "liveControlSmoke" && key !== "rawArtifacts" && key !== "workingAppRuntimeProof")
|
|
172
187
|
.map(([key]) => `${key}_failed`);
|
|
173
188
|
if (rawSessionArtifacts.length > 0) blockers.push("raw_session_artifacts_present");
|
|
189
|
+
if (evidenceScanDepthExceeded.length > 0) blockers.push("evidence_scan_depth_exceeded");
|
|
174
190
|
if (liveControlRequired && !checks.liveControlSmoke?.ok) blockers.push("approved_live_control_smoke_missing");
|
|
175
191
|
if (workingAppRuntimeProofRequired && workingAppRuntimeProof && !workingAppRuntimeProof.ok) {
|
|
176
192
|
blockers.push(...workingAppRuntimeProof.blockers);
|
|
@@ -188,7 +204,8 @@ export function runReleasePreflight(options: ReleasePreflightOptions = {}): Rele
|
|
|
188
204
|
checks,
|
|
189
205
|
blockers,
|
|
190
206
|
forbiddenClaims,
|
|
191
|
-
rawSessionArtifacts
|
|
207
|
+
rawSessionArtifacts,
|
|
208
|
+
evidenceScanDepthExceeded
|
|
192
209
|
};
|
|
193
210
|
|
|
194
211
|
if (report.artifactManifestPath) {
|
|
@@ -272,22 +289,54 @@ function findPackageRoot(start: string): string | null {
|
|
|
272
289
|
}
|
|
273
290
|
}
|
|
274
291
|
|
|
275
|
-
function
|
|
276
|
-
if (!evidenceDir || !existsSync(evidenceDir))
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
292
|
+
function scanEvidenceArtifacts(evidenceDir: string | undefined): EvidenceScanResult {
|
|
293
|
+
if (!evidenceDir || !existsSync(evidenceDir)) {
|
|
294
|
+
return { rawSessionArtifacts: [], depthLimitExceeded: [] };
|
|
295
|
+
}
|
|
296
|
+
const root = resolve(evidenceDir);
|
|
297
|
+
const rawSessionArtifacts: RawSessionArtifact[] = [];
|
|
298
|
+
const depthLimitExceeded: string[] = [];
|
|
299
|
+
const visit = (dir: string, depth: number): void => {
|
|
300
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
301
|
+
const absolutePath = join(dir, entry.name);
|
|
302
|
+
const relativePath = normalizePackagePath(relative(root, absolutePath));
|
|
303
|
+
if (entry.isSymbolicLink()) continue;
|
|
304
|
+
if (entry.isDirectory()) {
|
|
305
|
+
if (depth >= EVIDENCE_SCAN_MAX_DEPTH) {
|
|
306
|
+
depthLimitExceeded.push(relativePath);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
visit(absolutePath, depth + 1);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (!entry.isFile()) continue;
|
|
313
|
+
const artifact = rawArtifactForName(relativePath);
|
|
314
|
+
if (artifact) rawSessionArtifacts.push(artifact);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
visit(root, 0);
|
|
318
|
+
return {
|
|
319
|
+
rawSessionArtifacts: rawSessionArtifacts
|
|
320
|
+
.filter((entry) => entry !== null)
|
|
321
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
322
|
+
depthLimitExceeded: depthLimitExceeded.sort((left, right) => left.localeCompare(right))
|
|
323
|
+
};
|
|
282
324
|
}
|
|
283
325
|
|
|
284
326
|
function rawArtifactForName(name: string): RawSessionArtifact | null {
|
|
285
327
|
if (name === "release-preflight.json") return null;
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (extension === ".
|
|
290
|
-
if (extension === ".
|
|
328
|
+
const normalizedName = normalizePackagePath(name);
|
|
329
|
+
const extension = extname(normalizedName).toLowerCase();
|
|
330
|
+
const lowerName = normalizedName.toLowerCase();
|
|
331
|
+
if (extension === ".jsonl") return { name: normalizedName, reason: "raw_codex_jsonl" };
|
|
332
|
+
if (extension === ".sqlite" || extension === ".sqlite3" || extension === ".db"
|
|
333
|
+
|| lowerName.endsWith(".sqlite-wal") || lowerName.endsWith(".sqlite-shm")
|
|
334
|
+
|| lowerName.endsWith(".sqlite3-wal") || lowerName.endsWith(".sqlite3-shm")
|
|
335
|
+
|| lowerName.endsWith(".db-wal") || lowerName.endsWith(".db-shm")) {
|
|
336
|
+
return { name: normalizedName, reason: "sqlite_database" };
|
|
337
|
+
}
|
|
338
|
+
if (extension === ".png" || extension === ".jpg" || extension === ".jpeg" || extension === ".heic" || extension === ".webp") return { name: normalizedName, reason: "screenshot_or_image" };
|
|
339
|
+
if (extension === ".mov" || extension === ".mp4" || extension === ".webm") return { name: normalizedName, reason: "video_capture" };
|
|
291
340
|
return null;
|
|
292
341
|
}
|
|
293
342
|
|
|
@@ -6531,13 +6531,13 @@ function formatClaudeSessionInventoryMetadata(description: ClaudeSessionInventor
|
|
|
6531
6531
|
return [
|
|
6532
6532
|
`Claude session ID: ${description.sessionId}`,
|
|
6533
6533
|
`Ref: ${description.sourceRef}`,
|
|
6534
|
-
description.title ? `Title: ${description.title}` : null,
|
|
6535
|
-
description.project ? `Project: ${description.project}` : null,
|
|
6536
|
-
description.workspaceHint ? `Workspace: ${description.workspaceHint}` : null,
|
|
6534
|
+
description.title ? `Title: ${publicSafeText(description.title, 500)}` : null,
|
|
6535
|
+
description.project ? `Project: ${publicSafeText(description.project, 500)}` : null,
|
|
6536
|
+
description.workspaceHint ? `Workspace: ${publicSafeText(description.workspaceHint, 500)}` : null,
|
|
6537
6537
|
description.status ? `Status: ${description.status}` : null,
|
|
6538
6538
|
description.updatedAt ? `Updated: ${description.updatedAt}` : null,
|
|
6539
|
-
`Source
|
|
6540
|
-
description.sourceRefs.length ? `Source refs: ${description.sourceRefs.join(", ")}` : null,
|
|
6539
|
+
`Source ref: ${publicSourcePathRef(description.sourcePath)}`,
|
|
6540
|
+
description.sourceRefs.length ? `Source refs: ${description.sourceRefs.map((ref) => publicSafeText(ref, 180)).join(", ")}` : null,
|
|
6541
6541
|
"Proof boundary: read-only Claude metadata fixture inventory only; no private transcript content, live control, GUI mutation, parity, or cloud sync proof."
|
|
6542
6542
|
].filter(Boolean).join("\n");
|
|
6543
6543
|
}
|
|
@@ -11405,15 +11405,15 @@ export function expandSession(db: LooDatabase, options: ExpandSessionOptions): E
|
|
|
11405
11405
|
`Thread: ${description.title ?? description.threadId}`,
|
|
11406
11406
|
`Ref: ${description.sourceRef}`,
|
|
11407
11407
|
`ID: ${description.threadId}`,
|
|
11408
|
-
description.cwd ? `CWD: ${description.cwd}` : null,
|
|
11408
|
+
description.cwd ? `CWD: ${publicSafeText(description.cwd, 500)}` : null,
|
|
11409
11409
|
description.branch ? `Branch: ${description.branch}` : null,
|
|
11410
11410
|
description.gitSha ? `Git SHA: ${description.gitSha}` : null,
|
|
11411
|
-
description.summary ? `Summary: ${description.summary}` : null,
|
|
11411
|
+
description.summary ? `Summary: ${publicSafeText(description.summary, 2000)}` : null,
|
|
11412
11412
|
formatSessionMetadata(description.metadata),
|
|
11413
11413
|
`Plans: ${description.planCount}`,
|
|
11414
11414
|
`Touched files: ${description.touchedFiles.length}`,
|
|
11415
11415
|
`Tool calls: ${description.toolCallCount}`,
|
|
11416
|
-
`Source
|
|
11416
|
+
`Source ref: ${publicSourcePathRef(description.sourcePath)}`
|
|
11417
11417
|
].filter(Boolean).join("\n");
|
|
11418
11418
|
return {
|
|
11419
11419
|
sourceKind: "codex_thread",
|
|
@@ -11427,13 +11427,13 @@ export function expandSession(db: LooDatabase, options: ExpandSessionOptions): E
|
|
|
11427
11427
|
const text = [
|
|
11428
11428
|
`Thread: ${description.title ?? description.threadId}`,
|
|
11429
11429
|
`ID: ${description.threadId}`,
|
|
11430
|
-
description.cwd ? `CWD: ${description.cwd}` : null,
|
|
11430
|
+
description.cwd ? `CWD: ${publicSafeText(description.cwd, 500)}` : null,
|
|
11431
11431
|
description.branch ? `Branch: ${description.branch}` : null,
|
|
11432
11432
|
description.gitSha ? `Git SHA: ${description.gitSha}` : null,
|
|
11433
|
-
description.summary ? `Summary: ${description.summary}` : null,
|
|
11434
|
-
description.finalMessage ? `Final message: ${
|
|
11433
|
+
description.summary ? `Summary: ${publicSafeText(description.summary, profile.name === "evidence" ? 3200 : 1600)}` : null,
|
|
11434
|
+
description.finalMessage ? `Final message: ${publicSafeText(description.finalMessage, profile.name === "evidence" ? 3200 : 900)}` : null,
|
|
11435
11435
|
description.touchedFiles.length ? `Touched files:\n${formatTouchedFiles(description.touchedFiles, profile.name === "evidence" ? 50 : 12, profile.name === "evidence" ? 3200 : 900)}` : null,
|
|
11436
|
-
plans.length ? `Plans:\n${plans.map((plan) =>
|
|
11436
|
+
plans.length ? `Plans:\n${plans.map((plan) => publicSafeText(plan, profile.name === "evidence" ? 3200 : 1200)).join("\n\n")}` : null
|
|
11437
11437
|
].filter(Boolean).join("\n\n");
|
|
11438
11438
|
return {
|
|
11439
11439
|
sourceKind: "codex_thread",
|
|
@@ -11449,7 +11449,7 @@ function formatTouchedFiles(files: string[], limit: number, maxChars: number): s
|
|
|
11449
11449
|
const perPathLimit = maxChars > 1000 ? 180 : 120;
|
|
11450
11450
|
const visible: string[] = [];
|
|
11451
11451
|
for (const file of files.slice(0, limit)) {
|
|
11452
|
-
const next = `- ${
|
|
11452
|
+
const next = `- ${publicSafeText(file, perPathLimit)}`;
|
|
11453
11453
|
const hiddenIfAccepted = files.length - (visible.length + 1);
|
|
11454
11454
|
const markerIfAccepted = hiddenIfAccepted > 0 ? `- ... ${hiddenIfAccepted} more touched files omitted` : null;
|
|
11455
11455
|
const visibleMaxChars = markerIfAccepted ? Math.max(0, maxChars - markerIfAccepted.length - 1) : maxChars;
|
|
@@ -11552,7 +11552,7 @@ export function expandRecallRef(db: LooDatabase, options: {
|
|
|
11552
11552
|
const metadata = formatClaudeSessionInventoryMetadata(description);
|
|
11553
11553
|
const text = profile.name === "metadata"
|
|
11554
11554
|
? metadata
|
|
11555
|
-
: truncateByApproxTokens(`${metadata}\n\nSafe summary:\n${description.summary ?? ""}`, profile.tokenBudget);
|
|
11555
|
+
: truncateByApproxTokens(`${metadata}\n\nSafe summary:\n${publicSafeText(description.summary ?? "", profile.tokenBudget * 6)}`, profile.tokenBudget);
|
|
11556
11556
|
return {
|
|
11557
11557
|
sourceKind: "claude_session",
|
|
11558
11558
|
sourceRef: description.sourceRef,
|
|
@@ -11568,18 +11568,18 @@ export function expandRecallRef(db: LooDatabase, options: {
|
|
|
11568
11568
|
const metadata = [
|
|
11569
11569
|
`Summary ID: ${summary.summaryId}`,
|
|
11570
11570
|
`Ref: ${lcmSummaryRef(summary.sourcePath, summary.summaryId)}`,
|
|
11571
|
-
`Conversation: ${summary.conversationTitle ?? summary.conversationId}`,
|
|
11571
|
+
`Conversation: ${publicSafeText(summary.conversationTitle ?? String(summary.conversationId), 500)}`,
|
|
11572
11572
|
`Conversation ID: ${summary.conversationId}`,
|
|
11573
11573
|
summary.kind ? `Kind: ${summary.kind}` : null,
|
|
11574
11574
|
summary.depth !== null ? `Depth: ${summary.depth}` : null,
|
|
11575
11575
|
summary.tokenCount !== null ? `Token count: ${summary.tokenCount}` : null,
|
|
11576
|
-
summary.model ? `Model: ${summary.model}` : null,
|
|
11576
|
+
summary.model ? `Model: ${publicSafeText(summary.model, 120)}` : null,
|
|
11577
11577
|
summary.updatedAt ? `Updated: ${summary.updatedAt}` : null,
|
|
11578
|
-
`Source
|
|
11578
|
+
`Source ref: ${publicSourcePathRef(summary.sourcePath)}`
|
|
11579
11579
|
].filter(Boolean).join("\n");
|
|
11580
11580
|
const text = profile.name === "metadata"
|
|
11581
11581
|
? metadata
|
|
11582
|
-
: truncateByApproxTokens(`${metadata}\n\nContent:\n${summary.content}`, profile.tokenBudget);
|
|
11582
|
+
: truncateByApproxTokens(`${metadata}\n\nContent:\n${publicSafeText(summary.content, profile.tokenBudget * 6)}`, profile.tokenBudget);
|
|
11583
11583
|
return {
|
|
11584
11584
|
sourceKind: "lcm_summary",
|
|
11585
11585
|
sourceRef: lcmSummaryRef(summary.sourcePath, summary.summaryId),
|
|
@@ -11855,7 +11855,7 @@ function lcmSummaryRecord(path: string, row: Record<string, unknown>): LcmSummar
|
|
|
11855
11855
|
conversationTitle: nullableString(row.conversationTitle),
|
|
11856
11856
|
kind: nullableString(row.kind),
|
|
11857
11857
|
depth: row.depth === null || row.depth === undefined ? null : Number(row.depth),
|
|
11858
|
-
content:
|
|
11858
|
+
content: redactPublicSafeString(String(row.content ?? "")),
|
|
11859
11859
|
tokenCount: row.tokenCount === null || row.tokenCount === undefined ? null : Number(row.tokenCount),
|
|
11860
11860
|
createdAt: nullableString(row.createdAt),
|
|
11861
11861
|
updatedAt: nullableString(row.updatedAt),
|
|
@@ -11969,13 +11969,20 @@ function publicSourcePathRef(sourcePath: string): string {
|
|
|
11969
11969
|
}
|
|
11970
11970
|
|
|
11971
11971
|
function publicSafeText(value: string, maxChars = 500): string {
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11972
|
+
return truncate(redactPublicSafeString(value), maxChars);
|
|
11973
|
+
}
|
|
11974
|
+
|
|
11975
|
+
function redactPublicSafeString(value: string): string {
|
|
11976
|
+
const localPathRootPattern =
|
|
11977
|
+
"(?:\\/Volumes\\/|\\/(?:Users|home|root)\\/|\\/(?:private\\/)?(?:tmp|var)\\/|~\\/|(?<![A-Za-z])[A-Za-z]:[\\\\/])";
|
|
11978
|
+
const structuredLabelPattern = "[A-Za-z][A-Za-z0-9 _-]{0,32}:";
|
|
11979
|
+
const relativePathStartPattern = "(?:\\.{1,2}\\/|[A-Za-z0-9_.-]+\\/)";
|
|
11980
|
+
const omissionMarkerPattern = "\\+\\d+\\s+more\\b";
|
|
11981
|
+
const localPathTerminatorPattern =
|
|
11982
|
+
`(?=$|[\\r\\n"'\\\`)\\]}]|\\s+(?:${localPathRootPattern}|${relativePathStartPattern}|${omissionMarkerPattern}|${structuredLabelPattern}))`;
|
|
11983
|
+
const localPathPattern = new RegExp(`${localPathRootPattern}(?:(?!${localPathTerminatorPattern}).)+`, "g");
|
|
11984
|
+
const pathRedacted = value.replace(localPathPattern, "<redacted-path>");
|
|
11985
|
+
return redactSafeString(pathRedacted).replace(localPathPattern, "<redacted-path>");
|
|
11979
11986
|
}
|
|
11980
11987
|
|
|
11981
11988
|
function publicSafeSearchText(value: string, maxChars = 500): string {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "lossless-openclaw-orchestrator",
|
|
3
3
|
"name": "Lossless OpenClaw Orchestrator",
|
|
4
4
|
"description": "Collaborate with local Codex sessions through OpenClaw using local indexing, prepared-state recall, bounded expansion, and approval-gated dry-run/control boundaries.",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.4",
|
|
6
6
|
"kind": "tool",
|
|
7
7
|
"tools": {
|
|
8
8
|
"prefix": "loo_"
|