spexcode 0.7.0-next.6 → 0.7.0-next.8

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-application",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "Adopter-owned composition of session protocol and topology transactions.",
@@ -16,10 +16,10 @@
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
18
  "dependencies": {
19
- "@spexcode/session-events": "0.7.0-next.6",
20
- "@spexcode/session-protocol": "0.7.0-next.6",
21
- "@spexcode/session-runtime": "0.7.0-next.6",
22
- "@spexcode/session-topology": "0.7.0-next.6"
19
+ "@spexcode/session-events": "0.7.0-next.8",
20
+ "@spexcode/session-protocol": "0.7.0-next.8",
21
+ "@spexcode/session-runtime": "0.7.0-next.8",
22
+ "@spexcode/session-topology": "0.7.0-next.8"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-events",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "Append-only session facts and deterministic ordered replay inputs.",
@@ -15,7 +15,7 @@
15
15
  "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
- "dependencies": { "@spexcode/session-protocol": "0.7.0-next.6" },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.8" },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.16.0",
21
21
  "tsx": "^4.19.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-protocol",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "type": "module",
5
5
  "description": "A durable SQLite session message protocol.",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-runtime",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "Adopter-owned bindings between protocol addresses and native runtime identities.",
@@ -15,7 +15,7 @@
15
15
  "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
- "dependencies": { "@spexcode/session-protocol": "0.7.0-next.6" },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.8" },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.16.0",
21
21
  "tsx": "^4.19.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-selflaunch",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "A fail-closed self-launch adopter for the SpexCode session protocol.",
@@ -24,8 +24,8 @@
24
24
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
25
25
  },
26
26
  "dependencies": {
27
- "@spexcode/session-protocol": "0.7.0-next.6",
28
- "@spexcode/session-runtime": "0.7.0-next.6"
27
+ "@spexcode/session-protocol": "0.7.0-next.8",
28
+ "@spexcode/session-runtime": "0.7.0-next.8"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/session-topology",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "publishConfig": { "access": "public" },
5
5
  "type": "module",
6
6
  "description": "A neutral SQLite relation model for session addresses.",
@@ -15,7 +15,7 @@
15
15
  "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
16
  "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
17
  },
18
- "dependencies": { "@spexcode/session-protocol": "0.7.0-next.6" },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.8" },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.16.0",
21
21
  "tsx": "^4.19.2",
@@ -206,6 +206,7 @@ export type DispatchResult = {
206
206
  };
207
207
  export type HarnessDeliveryRecord = {
208
208
  session: string;
209
+ harness?: string;
209
210
  stopped?: boolean;
210
211
  archived?: boolean;
211
212
  worktreePath?: string;
@@ -18,7 +18,8 @@ import { runtimeRoot, mainCheckout, readConfig, sessionArtifactPath, spexcodeHom
18
18
  import { git } from '@spexcode/spec-core';
19
19
  import { shQuote } from './sh.js';
20
20
  import { detachedRuntimeGenerationToken, migrateLegacyDetachedRuntimeReceipt, processStartToken, verifyDetachedRuntime } from '@spexcode/spec-core';
21
- import { codexGenerationEndpoints, codexGenerationSocketPath, currentCodexGeneration, legacyCodexGenerationEndpoint, readCodexGenerationLedger, prepareCodexGenerationClose, resolveCodexGenerationForClose, resolveCodexGenerationForSession } from './codex-runtime-generations.js';
21
+ import { codexGenerationEndpoints, codexGenerationSocketPath, currentCodexGeneration, legacyCodexGenerationEndpoint, readCodexGenerationLedger, prepareCodexGenerationClose, resolveCodexGenerationForClose, resolveCodexGenerationForResume, resolveCodexGenerationForSession } from './codex-runtime-generations.js';
22
+ import { spawnDetachedRuntime } from './runtime-ownership.js';
22
23
  import { writeFileIfChanged } from './file-write.js';
23
24
  import { claudeTranscript, codexRolloutPath, codexTranscript, opencodeTranscript, piTranscript, unsupportedTranscript } from '@spexcode/transcript';
24
25
  import { harnessIdentity, HARNESS_IDENTITIES } from '@spexcode/spec-core';
@@ -2282,7 +2283,21 @@ async function deliverViaCodexAppServer(rec, text) {
2282
2283
  // the socket is PER-PROJECT (the runtime root), shared by every worktree's thread; the owned thread id on
2283
2284
  // the record picks out THIS session's thread.
2284
2285
  const runtimeDir = rec.runtimeDir ?? runtimeRoot();
2285
- const endpoint = rec.harnessSessionId ? codexEndpointForRecord(rec, runtimeDir) : currentCodexGeneration(runtimeDir);
2286
+ let endpoint = rec.harnessSessionId ? codexEndpointForRecord(rec, runtimeDir) : currentCodexGeneration(runtimeDir);
2287
+ // A generation may be reclaimed after rotation or a host restart while the session record remains valid.
2288
+ // Repair that stale route at the delivery boundary using the same exact-thread re-pin used by resume, so an
2289
+ // accepted message is not left indefinitely in the queue just because no later human resume was requested.
2290
+ if ((!endpoint || !existsSync(endpoint.socketPath)) && rec.harnessSessionId) {
2291
+ const command = codexBaseCmd(rec.launchCmd || 'codex');
2292
+ const env = { ...process.env };
2293
+ for (const key of sessionIdentityEnvVars())
2294
+ delete env[key];
2295
+ const start = async (candidate) => {
2296
+ await spawnDetachedRuntime({ cwd: runtimeDir, logFile: candidate.logFile, pidFile: candidate.pidFile,
2297
+ receiptFile: candidate.receiptFile, command, args: ['app-server', '--listen', `unix://${candidate.socketPath}`], env });
2298
+ };
2299
+ endpoint = await resolveCodexGenerationForResume(runtimeDir, rec.session, rec.harnessSessionId, start);
2300
+ }
2286
2301
  if (!endpoint)
2287
2302
  return { ok: false, error: `no exact Codex generation binding for session ${rec.session} — immediate poke unavailable` };
2288
2303
  const sock = endpoint.socketPath;
@@ -2297,6 +2312,14 @@ async function deliverViaCodexAppServer(rec, text) {
2297
2312
  return { ok: false, error: `${r.error} — immediate poke unavailable` };
2298
2313
  threadId = r.threadId;
2299
2314
  }
2315
+ const delivered = await sendCodexAppServerTurn(sock, threadId, text, rec.worktreePath, rec.mid);
2316
+ if (delivered.ok || rec.harness !== 'codex-headless' || !/not loaded in the app-server/u.test(delivered.error || ''))
2317
+ return delivered;
2318
+ // Headless Codex has no TUI resume step. An idle thread can be evicted from the shared server's loaded set;
2319
+ // reload the exact rollout, then retry the same turn once. This is idempotent and does not create a new thread.
2320
+ const reopened = await codexReopenThread(sock, threadId);
2321
+ if (!reopened.ok)
2322
+ return { ok: false, error: `${delivered.error}; ${reopened.error}` };
2300
2323
  return sendCodexAppServerTurn(sock, threadId, text, rec.worktreePath, rec.mid);
2301
2324
  }
2302
2325
  // idempotent replace of the content between sentinels; the user's own content above/below is preserved. The
@@ -1,4 +1,4 @@
1
- import { writeFileSync, mkdirSync, readFileSync, existsSync, readdirSync, renameSync, rmSync, rmdirSync } from 'node:fs';
1
+ import { writeFileSync, mkdirSync, readFileSync, existsSync, readdirSync, renameSync, rmSync, rmdirSync, copyFileSync, chmodSync } from 'node:fs';
2
2
  import { join, dirname, relative } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { execFileSync } from 'node:child_process';
@@ -37,6 +37,58 @@ const DISPATCH = join(PKG, 'hooks', 'dispatch.sh');
37
37
  // CLI code and keeps the source-workspace mid-merge guard (one line + exit 75), so every hook callback
38
38
  // inherits both.
39
39
  const SPEX = join(PKG, 'bin', 'spex.mjs');
40
+ const CORE_TEMPLATE = join(PKG, 'templates', 'spec', 'project', '.plugins', 'core');
41
+ // Core hook handlers are shipped executable protocol, not adopter-owned plugin variants. Reconcile only the
42
+ // known `core/` subtree before compiling the manifest so a project seeded by an older toolchain cannot keep
43
+ // invoking a retired lifecycle writer. User plugins live outside this allowlist and are never enumerated.
44
+ function refreshCorePluginHandlers(proj) {
45
+ if (!existsSync(CORE_TEMPLATE))
46
+ return [];
47
+ const specDir = join(proj, '.spec');
48
+ const roots = existsSync(specDir)
49
+ ? readdirSync(specDir, { withFileTypes: true })
50
+ .filter((entry) => entry.isDirectory() && existsSync(join(specDir, entry.name, '.plugins', 'core')))
51
+ .map((entry) => join(specDir, entry.name, '.plugins', 'core'))
52
+ : [];
53
+ const handlers = [];
54
+ const walk = (dir, prefix = '') => {
55
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
56
+ const rel = join(prefix, entry.name);
57
+ if (entry.isDirectory())
58
+ walk(join(dir, entry.name), rel);
59
+ else if (entry.isFile() && entry.name.endsWith('.sh'))
60
+ handlers.push(rel);
61
+ }
62
+ };
63
+ walk(CORE_TEMPLATE);
64
+ const refreshed = [];
65
+ for (const root of roots)
66
+ for (const rel of handlers) {
67
+ const source = join(CORE_TEMPLATE, rel);
68
+ const dest = join(root, rel);
69
+ let current = null;
70
+ try {
71
+ current = readFileSync(dest);
72
+ }
73
+ catch { }
74
+ if (current?.equals(readFileSync(source)))
75
+ continue;
76
+ mkdirSync(dirname(dest), { recursive: true });
77
+ const temp = `${dest}.spexcode-${process.pid}`;
78
+ try {
79
+ copyFileSync(source, temp);
80
+ chmodSync(temp, 0o755);
81
+ renameSync(temp, dest);
82
+ refreshed.push(relative(proj, dest));
83
+ }
84
+ finally {
85
+ rmSync(temp, { force: true });
86
+ }
87
+ }
88
+ if (refreshed.length)
89
+ console.log(`✓ refreshed core plugin handlers (${refreshed.join(', ')})`);
90
+ return refreshed;
91
+ }
40
92
  // the manifest + content-hash marker + plugin-folder ledger land in the materialized TREE's own slot of the
41
93
  // GLOBAL per-project store (layout.treeSlotDir — trees/<enc-worktree>), NOT the worktree and NOT one shared
42
94
  // per-project file: each is a pure function of ONE tree's .plugins, and the old single slot let the last-
@@ -321,6 +373,7 @@ export function dematerialize(proj = process.cwd(), arts = { skills: [], agents:
321
373
  // the whole pay-per-change materialize. proj defaults to cwd. Its receipt is populated at each successful
322
374
  // write so callers report the actual selected footprint instead of maintaining a second artifact inventory.
323
375
  export function materialize(proj = process.cwd()) {
376
+ refreshCorePluginHandlers(proj);
324
377
  const rt = treeSlotDir(proj); // this tree's slot in the global store, not the worktree
325
378
  mkdirSync(rt, { recursive: true });
326
379
  const planted = [];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-cli",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "type": "module",
5
5
  "description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
6
6
  "bin": {
@@ -35,12 +35,12 @@
35
35
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@spexcode/session-application": "0.7.0-next.6",
39
- "@spexcode/session-selflaunch": "0.7.0-next.6",
40
- "@spexcode/spec-core": "0.7.0-next.6",
41
- "@spexcode/spec-eval": "0.7.0-next.6",
42
- "@spexcode/spec-forge": "0.7.0-next.6",
43
- "@spexcode/transcript": "0.7.0-next.6",
38
+ "@spexcode/session-application": "0.7.0-next.8",
39
+ "@spexcode/session-selflaunch": "0.7.0-next.8",
40
+ "@spexcode/spec-core": "0.7.0-next.8",
41
+ "@spexcode/spec-eval": "0.7.0-next.8",
42
+ "@spexcode/spec-forge": "0.7.0-next.8",
43
+ "@spexcode/transcript": "0.7.0-next.8",
44
44
  "smol-toml": "^1.8.0"
45
45
  },
46
46
  "devDependencies": {
@@ -19,9 +19,15 @@ Land the current SpexCode session's branch; do not dispatch another merge reques
19
19
  3. Immediately before landing, verify
20
20
  `git merge-base --is-ancestor <source-head> <session-head>`. If it fails, sync again. A clean textual merge
21
21
  is not product proof.
22
- 4. In the source-of-truth checkout, make one `--no-ff` merge of the already-synced session tip. Do not resolve
23
- conflicts there. If unrelated dirty work prevents the merge, preserve it byte-for-byte and report the exact
24
- overlap rather than forcing, resetting, or committing it.
22
+ 4. Land with one `--no-ff` merge of the already-synced session tip, without touching the source checkout's
23
+ dirty work. Git refuses a merge over a dirty index, so do NOT clear it: add a temporary detached worktree of
24
+ the source head (`git worktree add --detach <tmp> <source-branch>`), make the `--no-ff` merge there, then
25
+ fast-forward the source checkout to that commit (`git merge --ff-only <tmp-head>`) and remove the temporary
26
+ worktree. A fast-forward never rewrites a path the merge did not change, so user-owned dirty files keep
27
+ their bytes and their staged/unstaged state. Never `git restore`, `reset`, `checkout`, `stash`, or overwrite
28
+ a user-owned path — an unstaged edit has no blob anywhere, so "save and put back" loses it, and a copy of the
29
+ same path from another worktree is a different version, not a restoration. Do not resolve conflicts in the
30
+ source checkout; if the merge genuinely overlaps a user-owned path, stop and report the exact overlap.
25
31
  5. Verify the source checkout has no `MERGE_HEAD`, the session tip is its ancestor, unrelated dirty
26
32
  fingerprints are unchanged, and the post-merge gates pass. Push the source-of-truth branch only after
27
33
  those checks.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-core",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "type": "module",
5
5
  "description": "SpexCode's dependency-minimal spec graph core.",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-eval",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
27
27
  },
28
28
  "dependencies": {
29
- "@spexcode/spec-core": "0.7.0-next.6"
29
+ "@spexcode/spec-core": "0.7.0-next.8"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^20.16.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/spec-forge",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test": "tsx --test src/*.test.ts"
23
23
  },
24
24
  "dependencies": {
25
- "@spexcode/spec-core": "0.7.0-next.6"
25
+ "@spexcode/spec-core": "0.7.0-next.8"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^20.16.0",
@@ -50,6 +50,30 @@ const compact = (value) => {
50
50
  return String(value);
51
51
  }
52
52
  };
53
+ // A RESULT IS WHAT THE TOOL SAID, NOT ITS WIRE SHAPE. Every harness that records a result as content blocks
54
+ // (Claude's tool_result content, Codex's input_text output blocks, MCP results everywhere) means the text of
55
+ // those blocks, with their line breaks; encoding the block list itself as JSON would show the reader escaped
56
+ // newlines inside a JSON shell. A block that is not text — an image, a reference — is named, not dumped.
57
+ const resultText = (value) => {
58
+ if (typeof value === 'string')
59
+ return value;
60
+ if (!Array.isArray(value))
61
+ return compact(value);
62
+ return value.map((blockValue) => {
63
+ const block = object(blockValue);
64
+ if (!block)
65
+ return compact(blockValue);
66
+ const text = string(block.text);
67
+ if (text !== null)
68
+ return text;
69
+ const type = string(block.type);
70
+ if (type === 'image')
71
+ return '[image]';
72
+ if (type)
73
+ return `[${type}]`;
74
+ return compact(blockValue);
75
+ }).join('\n');
76
+ };
53
77
  const lineCount = (value) => value ? value.split(/\r?\n/).length : 0;
54
78
  // --- the seven native shapes -------------------------------------------------------------------------------
55
79
  export function claudeEvent(value) {
@@ -82,7 +106,7 @@ export function claudeEvent(value) {
82
106
  const outputs = blocks.flatMap((block) => {
83
107
  const b = object(block);
84
108
  const id = string(b?.tool_use_id);
85
- return b?.type === 'tool_result' && id ? [{ id, text: compact(b?.content ?? ''), ...(b?.is_error === true ? { outcome: 'failed' } : {}) }] : [];
109
+ return b?.type === 'tool_result' && id ? [{ id, text: resultText(b?.content ?? ''), ...(b?.is_error === true ? { outcome: 'failed' } : {}) }] : [];
86
110
  });
87
111
  if (outputs.length)
88
112
  return { at: eventAt, turn: null, toolOutputs: outputs };
@@ -142,7 +166,7 @@ export function codexEvent(value) {
142
166
  if (entry.type === 'response_item' && (type === 'custom_tool_call_output' || type === 'function_call_output')) {
143
167
  const id = string(payload.call_id ?? payload.id);
144
168
  const output = payload.output ?? payload.result ?? '';
145
- return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: compact(output) }] } : null;
169
+ return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: resultText(output) }] } : null;
146
170
  }
147
171
  return null;
148
172
  }
@@ -211,7 +235,7 @@ export function codexAppServerEvent(value) {
211
235
  if (output === undefined || output === null) {
212
236
  return outcome ? { at: eventAt, turn: null, toolOutputs: [{ id, text: '', outcome }] } : { at: eventAt, turn: null };
213
237
  }
214
- return { at: eventAt, turn: null, toolOutputs: [{ id, text: compact(output), ...(outcome ? { outcome } : {}) }] };
238
+ return { at: eventAt, turn: null, toolOutputs: [{ id, text: resultText(output), ...(outcome ? { outcome } : {}) }] };
215
239
  }
216
240
  // Agent-message deltas are fragments of one native item. The closure remembers only that item's text and
217
241
  // re-emits its native id, allowing IntervalCollector to replace the earlier turn in place.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/transcript",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "type": "module",
5
5
  "description": "Normalized agent transcripts: one parser per harness, a bounded interval reader over a native thread file or an in-memory event stream, and the full/delta frame protocol every transport and renderer share.",
6
6
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spexcode",
3
- "version": "0.7.0-next.6",
3
+ "version": "0.7.0-next.8",
4
4
  "type": "module",
5
5
  "description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
6
6
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "prepack": "node scripts/sync-init-plugins.mjs --check && node scripts/prepack.mjs"
45
45
  },
46
46
  "dependencies": {
47
- "@spexcode/spec-cli": "0.7.0-next.6"
47
+ "@spexcode/spec-cli": "0.7.0-next.8"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"