@inetafrica/open-claudia 3.0.33 → 3.0.35

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.0.35 — Spaces comments are readable rich text, not a run-on wall
4
+
5
+ - **The bot's Spaces replies rendered as unreadable walls.** `buildCommentContent` posted the entire reply as one text node inside one paragraph. Spaces comments are TipTap/ProseMirror docs: inside a single text node newlines don't render, so paragraphs, numbered lists and `---` separators collapsed into one run-on blob — and the model's markdown (`**bold**`, `*italic*`, backticks) printed its asterisks literally.
6
+ - **Outgoing comments now convert markdown → a real ProseMirror doc.** Blank lines become paragraphs, single newlines hardBreaks; `**`/`*`/`` ` `` become bold/italic/code marks (word-bounded, so snake_case identifiers stay plain); `-`/`1.` runs become real bullet/ordered lists; `#`–`###` become headings; ``` fences become code blocks; `---` becomes a divider. Every emitted node type was verified against the Spaces frontend editor schema (StarterKit + HorizontalRule + CodeBlockLowlight), so nothing renders broken.
7
+ - **Mention chips unchanged.** The proven tag-chip shape still leads the first paragraph, so notification fan-out and the "@Name text…" lead-in behave exactly as before. `flattenContent` also learned `codeBlock`/`horizontalRule` so the bot reads its own comments back cleanly.
8
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change. Covered by new `test-spaces-comment-content.js` in the pretest chain. Bots pick this up on their next upgrade, together with v3.0.34's /upgrade redelivery guard.
9
+
10
+ ## v3.0.34 — /upgrade no longer triggers a rollout storm
11
+
12
+ - **One `/upgrade` used to mean up to five rollouts.** On AgentSpace pods the control plane answers `/upgrade` by rolling the deployment, which SIGTERMs the bot mid-turn. Telegram long-polling keeps no persisted offset, so the still-unconfirmed `/upgrade` update was redelivered to the freshly booted pod — which triggered *another* rollout, and so on. Observed on rahilcoder 2026-07-17: deployment revisions 47–51 (five rollouts in three minutes) from a single command; every earlier `/upgrade` had silently double-fired the same way (revs 42/43, 45/46).
13
+ - **Fix: an upgrade-in-flight marker (`upgrade-in-flight.json` in the config dir, 10-minute freshness) guards the handler.** A redelivered or impatiently re-run `/upgrade` inside the window is acknowledged but not re-triggered: if the pod has already restarted since the request it replies "that upgrade already went through", otherwise "already in flight". The marker is cleared on a definitive trigger failure so an immediate retry stays possible, and simply ages out otherwise.
14
+ - **The ack now goes out BEFORE the rollout is requested.** Previously the "Upgrade requested" confirmation was sent after the control-plane call, so the SIGTERM routinely killed it — Rahil saw reboot greetings *before* any acknowledgement. Ack-first means the user always sees the confirmation, then the reboot.
15
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds FROM the existing `:base`. Existing pods pick this up on their next upgrade — that upgrade itself will still multi-fire (old code is doing the triggering), all later ones are single-shot.
16
+
3
17
  ## v3.0.33 — Full sudo inside the pod (re-cut of v3.0.32; the pin guard bit us again)
4
18
 
5
19
  - **`claudia` now has `NOPASSWD:ALL` sudo in the pod image.** The rebuilt `:base` widens the baked sudoers from the old command allow-list to full passwordless sudo, so the agent can install packages and operate inside its own container like a normal machine. This pairs with agent-space's IaaS-style security-policy system: the blast radius is fenced at the pod's NetworkPolicy (egress/ingress presets, DB-rendered), not by crippling the user inside the container.
@@ -89,8 +89,9 @@ function flattenContent(node) {
89
89
  if (t === "text") return node.text || "";
90
90
  if (t === "mention") return `@${node.attrs?.label || node.attrs?.id || ""}`;
91
91
  if (t === "hardBreak") return "\n";
92
+ if (t === "horizontalRule") return "---\n";
92
93
  const inner = (node.content || []).map(flattenContent).join("");
93
- if (["paragraph", "heading", "listItem", "blockquote"].includes(t)) return inner + "\n";
94
+ if (["paragraph", "heading", "listItem", "blockquote", "codeBlock"].includes(t)) return inner + "\n";
94
95
  return inner;
95
96
  }
96
97
 
@@ -98,17 +99,126 @@ function contentText(raw) {
98
99
  return flattenContent(raw).trim();
99
100
  }
100
101
 
102
+ // Inline markdown → ProseMirror inline nodes. Supports `code`, **bold** and
103
+ // *italic*/_italic_ (word-bounded, so snake_case identifiers and stray
104
+ // asterisks in prose survive untouched). One level only — chat comments
105
+ // don't need nesting, and a wrong guess renders worse than plain text.
106
+ const INLINE_TOKEN_RE = /(`[^`\n]+`)|(\*\*(?=\S)[^*\n]*?\S\*\*)|(\*(?=\S)[^*\n]*?\S\*)|((?<![\w`])_(?=\S)[^_\n]*?\S_(?![\w`]))/g;
107
+ function inlineNodes(text) {
108
+ const out = [];
109
+ const push = (t, marks) => { if (t) out.push(marks ? { type: "text", text: t, marks } : { type: "text", text: t }); };
110
+ let last = 0;
111
+ let m;
112
+ INLINE_TOKEN_RE.lastIndex = 0;
113
+ while ((m = INLINE_TOKEN_RE.exec(text))) {
114
+ push(text.slice(last, m.index));
115
+ if (m[1]) push(m[1].slice(1, -1), [{ type: "code" }]);
116
+ else if (m[2]) push(m[2].slice(2, -2), [{ type: "bold" }]);
117
+ else if (m[3]) push(m[3].slice(1, -1), [{ type: "italic" }]);
118
+ else push(m[4].slice(1, -1), [{ type: "italic" }]);
119
+ last = m.index + m[0].length;
120
+ }
121
+ push(text.slice(last));
122
+ return out;
123
+ }
124
+
125
+ // Block-level markdown → ProseMirror blocks. The model writes markdown; Spaces
126
+ // renders TipTap docs — posting the raw text as one text node collapses every
127
+ // newline and prints the asterisks literally (the unreadable-wall bug). Only
128
+ // node types present in the Spaces editor schema (StarterKit + HorizontalRule
129
+ // + CodeBlockLowlight) are emitted, so nothing renders broken.
130
+ function markdownBlocks(text) {
131
+ const blocks = [];
132
+ const lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
133
+ let para = [];
134
+ const flushPara = () => {
135
+ if (!para.length) return;
136
+ const content = [];
137
+ para.forEach((ln, idx) => {
138
+ if (idx > 0) content.push({ type: "hardBreak" });
139
+ content.push(...inlineNodes(ln));
140
+ });
141
+ para = [];
142
+ if (content.length) blocks.push({ type: "paragraph", content });
143
+ };
144
+ let i = 0;
145
+ while (i < lines.length) {
146
+ const trimmed = lines[i].trim();
147
+ if (!trimmed) { flushPara(); i += 1; continue; }
148
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) { flushPara(); blocks.push({ type: "horizontalRule" }); i += 1; continue; }
149
+ if (trimmed.startsWith("```")) {
150
+ flushPara();
151
+ const language = trimmed.slice(3).trim();
152
+ const buf = [];
153
+ i += 1;
154
+ while (i < lines.length && !lines[i].trim().startsWith("```")) { buf.push(lines[i]); i += 1; }
155
+ i += 1;
156
+ blocks.push({
157
+ type: "codeBlock",
158
+ attrs: { language: language || null },
159
+ ...(buf.length ? { content: [{ type: "text", text: buf.join("\n") }] } : {}),
160
+ });
161
+ continue;
162
+ }
163
+ if (/^[-*•]\s+/.test(trimmed)) {
164
+ flushPara();
165
+ const items = [];
166
+ while (i < lines.length && /^[-*•]\s+/.test(lines[i].trim())) {
167
+ const inner = inlineNodes(lines[i].trim().replace(/^[-*•]\s+/, ""));
168
+ items.push({ type: "listItem", content: [inner.length ? { type: "paragraph", content: inner } : { type: "paragraph" }] });
169
+ i += 1;
170
+ }
171
+ blocks.push({ type: "bulletList", content: items });
172
+ continue;
173
+ }
174
+ if (/^\d+[.)]\s+/.test(trimmed)) {
175
+ flushPara();
176
+ const start = parseInt(trimmed, 10);
177
+ const items = [];
178
+ while (i < lines.length && /^\d+[.)]\s+/.test(lines[i].trim())) {
179
+ const inner = inlineNodes(lines[i].trim().replace(/^\d+[.)]\s+/, ""));
180
+ items.push({ type: "listItem", content: [inner.length ? { type: "paragraph", content: inner } : { type: "paragraph" }] });
181
+ i += 1;
182
+ }
183
+ blocks.push({ type: "orderedList", attrs: { start: Number.isNaN(start) ? 1 : start }, content: items });
184
+ continue;
185
+ }
186
+ const heading = trimmed.match(/^(#{1,3})\s+(.*)$/);
187
+ if (heading) {
188
+ flushPara();
189
+ const inner = inlineNodes(heading[2]);
190
+ blocks.push({ type: "heading", attrs: { level: heading[1].length }, ...(inner.length ? { content: inner } : {}) });
191
+ i += 1;
192
+ continue;
193
+ }
194
+ para.push(lines[i]);
195
+ i += 1;
196
+ }
197
+ flushPara();
198
+ return blocks;
199
+ }
200
+
101
201
  // Build the stringified ProseMirror doc for an outgoing comment. Optional
102
202
  // leading @mention chips (each {userId,label}) drive Spaces' notification
103
- // fan-out; the tool proved this exact node shape renders as tag chips.
203
+ // fan-out; the tool proved this exact node shape renders as tag chips. Chips
204
+ // are prepended into the first paragraph so the visual "@Name text…" lead-in
205
+ // stays identical to the pre-markdown behaviour.
104
206
  function buildCommentContent(text, mentions = []) {
105
- const inline = [];
207
+ const chips = [];
106
208
  for (const m of mentions) {
107
- inline.push({ type: "mention", attrs: { id: m.userId, label: m.label, mentionSuggestionChar: "@" } });
108
- inline.push({ type: "text", text: " " });
209
+ chips.push({ type: "mention", attrs: { id: m.userId, label: m.label, mentionSuggestionChar: "@" } });
210
+ chips.push({ type: "text", text: " " });
211
+ }
212
+ const blocks = markdownBlocks(text);
213
+ if (chips.length) {
214
+ if (blocks.length && blocks[0].type === "paragraph") {
215
+ blocks[0].content = [...chips, ...(blocks[0].content || [])];
216
+ } else {
217
+ blocks.unshift({ type: "paragraph", content: chips });
218
+ }
109
219
  }
110
- if (text) inline.push({ type: "text", text });
111
- return JSON.stringify({ type: "doc", content: [{ type: "paragraph", content: inline }] });
220
+ if (!blocks.length) blocks.push({ type: "paragraph" });
221
+ return JSON.stringify({ type: "doc", content: blocks });
112
222
  }
113
223
 
114
224
  class SpacesClient {
@@ -201,4 +311,4 @@ class SpacesClient {
201
311
  }
202
312
  }
203
313
 
204
- module.exports = { SpacesClient, flattenContent, contentText, buildCommentContent, asArray };
314
+ module.exports = { SpacesClient, flattenContent, contentText, buildCommentContent, markdownBlocks, inlineNodes, asArray };
package/core/handlers.js CHANGED
@@ -29,6 +29,7 @@ const keyring = require("./keyring");
29
29
  const { redactSensitive, registerSecrets } = require("./redact");
30
30
  const { runDoctorChecks, formatDoctorReport } = require("./doctor");
31
31
  const { isNewerVersion } = require("./version");
32
+ const { recordUpgradeRequested, upgradeInFlightSince, clearUpgradeMarker } = require("./upgrade-marker");
32
33
  const jobs = require("./jobs");
33
34
  const scheduler = require("./scheduler");
34
35
  const skillsLib = require("./skills");
@@ -672,23 +673,45 @@ register({
672
673
  // delegate to the control plane so the deployment can be rolled with a
673
674
  // fresh image pull. The local npm path can't do that.
674
675
  if (process.env.AGENTSPACE_POD_TOKEN && process.env.AGENTSPACE_API_URL) {
676
+ // Redelivery guard: the rollout SIGTERMs this pod mid-turn and Telegram
677
+ // redelivers the still-unconfirmed /upgrade update to the rebooted pod.
678
+ // Unguarded, each replay triggered another rollout (one /upgrade was
679
+ // observed to cause five). A fresh marker means the request was already
680
+ // honoured — by this boot or the previous one.
681
+ const since = upgradeInFlightSince();
682
+ if (since) {
683
+ const bootedAt = Date.now() - Math.round(process.uptime() * 1000);
684
+ if (since < bootedAt) {
685
+ await send(`That upgrade already went through — I restarted onto the freshly pulled image and I'm running v${CURRENT_VERSION}. Skipping the duplicate trigger.`);
686
+ } else {
687
+ const mins = Math.max(1, Math.round((Date.now() - since) / 60000));
688
+ await send(`An upgrade rollout is already in flight (requested ~${mins} min ago) — not triggering another. If I seem stuck, the guard clears 10 minutes after the request.`);
689
+ }
690
+ return;
691
+ }
692
+ // Ack BEFORE triggering: once the control plane rolls the deployment we
693
+ // can be SIGTERM'd at any moment, and an unsent ack dies with us.
694
+ await send("Upgrade requested — AgentSpace will pull latest image and restart, see you in a minute.");
695
+ recordUpgradeRequested();
675
696
  try {
676
697
  const result = await requestAgentSpaceUpgrade();
677
698
  if (result && (result.status === 202 || result.status === 200)) {
678
- await send("Upgrade requested — AgentSpace will pull latest image and restart, see you in a minute.");
679
699
  return;
680
700
  }
681
701
  // A client-side timeout is not a failure: the control plane triggers the
682
702
  // rollout fire-and-forget, and a pod being rolled is exactly when its own
683
703
  // in-flight request gets cut off. Framing it as "failed" invites a retry
684
- // storm of redundant rollouts, so surface it as pending instead.
704
+ // storm of redundant rollouts, so surface it as pending instead — and
705
+ // keep the marker, since the rollout is most likely underway.
685
706
  if (result && result.status === 0 && /timeout/i.test(result.body || "")) {
686
707
  await send("Upgrade request sent but the reply timed out — likely because I'm already restarting. Give it a minute before re-running; I should come back on the new image.");
687
708
  return;
688
709
  }
710
+ clearUpgradeMarker();
689
711
  await send(`Upgrade request failed (status ${result?.status || "?"}). ${(result?.body || "").slice(0, 300)}`);
690
712
  return;
691
713
  } catch (e) {
714
+ clearUpgradeMarker();
692
715
  await send(`Upgrade request error: ${e.message || e}`);
693
716
  return;
694
717
  }
@@ -0,0 +1,42 @@
1
+ // Guards /upgrade on AgentSpace-managed pods against the redelivery loop:
2
+ // the control-plane rollout SIGTERMs this pod mid-turn, Telegram long-polling
3
+ // keeps no persisted offset, so the still-unconfirmed /upgrade update is
4
+ // redelivered to the freshly booted pod — which would trigger ANOTHER rollout
5
+ // (observed: one /upgrade → five rollouts in three minutes). A fresh marker
6
+ // on disk tells the handler the request was already honoured.
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const { CONFIG_DIR } = require("./config");
11
+
12
+ const FILE = path.join(CONFIG_DIR, "upgrade-in-flight.json");
13
+ // An image pull + pod recreate completes well inside this window; after it a
14
+ // re-run of /upgrade is treated as a genuine new request.
15
+ const FRESH_MS = 10 * 60 * 1000;
16
+
17
+ // Call right before triggering the rollout. Best-effort: never throws.
18
+ function recordUpgradeRequested() {
19
+ try {
20
+ fs.writeFileSync(FILE, JSON.stringify({ at: Date.now() }));
21
+ } catch (e) { /* the recorder must never itself throw */ }
22
+ }
23
+
24
+ // Epoch ms of the in-flight upgrade request, or null when none is fresh.
25
+ // Deliberately does NOT clear the file: Telegram may redeliver more than one
26
+ // unconfirmed copy of the command, so the guard must hold until it ages out.
27
+ function upgradeInFlightSince() {
28
+ try {
29
+ const rec = JSON.parse(fs.readFileSync(FILE, "utf8"));
30
+ if (rec && typeof rec.at === "number" && Date.now() - rec.at <= FRESH_MS) {
31
+ return rec.at;
32
+ }
33
+ } catch (e) { /* absent/unreadable ⇒ no upgrade in flight */ }
34
+ return null;
35
+ }
36
+
37
+ // Call when the trigger definitively failed, so an immediate retry is allowed.
38
+ function clearUpgradeMarker() {
39
+ try { fs.unlinkSync(FILE); } catch (e) {}
40
+ }
41
+
42
+ module.exports = { recordUpgradeRequested, upgradeInFlightSince, clearUpgradeMarker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.0.33",
3
+ "version": "3.0.35",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "setup": "node setup.js",
11
11
  "start": "node bot.js",
12
- "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js",
12
+ "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js",
13
13
  "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js"
14
14
  },
15
15
  "files": [
@@ -32,16 +32,15 @@ assert.doesNotMatch(setup, /Your Claude Code bot is connected|Description=Claude
32
32
 
33
33
  const pkg = JSON.parse(read("package.json"));
34
34
  assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
35
- // 3.0.33 approved by Sumeet 2026-07-16 ("build and push it all out"): full-sudo
36
- // pod image — the rebuilt :base widens claudia's sudoers from a command
37
- // allow-list to NOPASSWD:ALL so the agent can operate its own container like a
38
- // normal machine (pairs with agent-space's IaaS-style security policy, which
39
- // fences the blast radius at the NetworkPolicy instead of crippling the user).
40
- // Re-cut of v3.0.32, whose build tripped THIS guard again — package.json was
41
- // bumped but the pin below wasn't, so pretest asserted 3.0.31, the build job
42
- // failed, docker was skipped, and nothing was published (same failure mode as
43
- // v3.0.30 -> v3.0.31).
44
- assert.strictEqual(pkg.version, "3.0.33", "release version must remain unchanged without explicit approval");
35
+ // 3.0.35 approved by Sumeet 2026-07-17 ("yes will this also ship the issue of
36
+ // new lines not showing properly?"): Spaces comments become real ProseMirror
37
+ // docs. buildCommentContent used to post the whole reply as ONE text node in
38
+ // ONE paragraph, so TipTap collapsed every newline and printed markdown
39
+ // asterisks literally — the unreadable-wall screenshot from the euvy bot
40
+ // thread. Now markdown converts to paragraphs/hardBreaks, bold/italic/code
41
+ // marks, bullet/ordered lists, headings, code blocks and dividers — all node
42
+ // types verified present in the Spaces editor schema.
43
+ assert.strictEqual(pkg.version, "3.0.35", "release version must remain unchanged without explicit approval");
45
44
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
46
45
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
47
46
  }