spexcode 0.6.8 → 0.7.0-next.0
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/node_modules/@spexcode/session-application/package.json +5 -5
- package/node_modules/@spexcode/session-events/package.json +2 -2
- package/node_modules/@spexcode/session-protocol/package.json +1 -1
- package/node_modules/@spexcode/session-runtime/package.json +2 -2
- package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +12 -1
- package/node_modules/@spexcode/session-selflaunch/dist/locality.js +62 -10
- package/node_modules/@spexcode/session-selflaunch/package.json +3 -3
- package/node_modules/@spexcode/session-topology/package.json +2 -2
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +127 -10
- package/node_modules/@spexcode/spec-cli/dist/cli.js +19 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.js +14 -2
- package/node_modules/@spexcode/spec-cli/dist/guide.js +9 -11
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +11 -5
- package/node_modules/@spexcode/spec-cli/dist/harness.js +185 -28
- package/node_modules/@spexcode/spec-cli/dist/host.js +5 -1
- package/node_modules/@spexcode/spec-cli/dist/index.js +21 -65
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +2 -1
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +45 -14
- package/node_modules/@spexcode/spec-cli/dist/opencode.js +10 -2
- package/node_modules/@spexcode/spec-cli/dist/pi-harness.js +18 -5
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +27 -0
- package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +57 -0
- package/node_modules/@spexcode/spec-cli/dist/session-transcript.js +137 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +4 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +103 -35
- package/node_modules/@spexcode/spec-cli/dist/shim-runtime.js +14 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +1 -1
- package/node_modules/@spexcode/spec-cli/package.json +8 -6
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +0 -1
- package/node_modules/@spexcode/spec-core/package.json +1 -1
- package/node_modules/@spexcode/spec-core/templates/spexcode.json +36 -13
- package/node_modules/@spexcode/spec-eval/package.json +2 -2
- package/node_modules/@spexcode/spec-forge/package.json +2 -2
- package/node_modules/@spexcode/transcript/dist/frames.d.ts +44 -0
- package/node_modules/@spexcode/transcript/dist/frames.js +85 -0
- package/node_modules/@spexcode/transcript/dist/index.d.ts +5 -0
- package/node_modules/@spexcode/transcript/dist/index.js +5 -0
- package/node_modules/@spexcode/transcript/dist/live.d.ts +16 -0
- package/node_modules/@spexcode/transcript/dist/live.js +65 -0
- package/node_modules/@spexcode/transcript/dist/parsers.d.ts +47 -0
- package/node_modules/@spexcode/transcript/dist/parsers.js +285 -0
- package/node_modules/@spexcode/transcript/dist/readers.d.ts +10 -0
- package/node_modules/@spexcode/transcript/dist/readers.js +296 -0
- package/node_modules/@spexcode/{spec-cli/dist/transcript-reader.d.ts → transcript/dist/turns.d.ts} +11 -5
- package/node_modules/@spexcode/transcript/dist/turns.js +11 -0
- package/node_modules/@spexcode/transcript/package.json +30 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +3 -3
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +0 -27
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +0 -475
- package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +0 -10
- package/node_modules/@spexcode/spec-cli/dist/session-execution.js +0 -70
- package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +0 -251
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-application",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-next.0",
|
|
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.
|
|
20
|
-
"@spexcode/session-protocol": "0.
|
|
21
|
-
"@spexcode/session-runtime": "0.
|
|
22
|
-
"@spexcode/session-topology": "0.
|
|
19
|
+
"@spexcode/session-events": "0.7.0-next.0",
|
|
20
|
+
"@spexcode/session-protocol": "0.7.0-next.0",
|
|
21
|
+
"@spexcode/session-runtime": "0.7.0-next.0",
|
|
22
|
+
"@spexcode/session-topology": "0.7.0-next.0"
|
|
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.
|
|
3
|
+
"version": "0.7.0-next.0",
|
|
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.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.0" },
|
|
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-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-next.0",
|
|
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.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.0" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -10,11 +10,22 @@ export interface FilesystemClassification {
|
|
|
10
10
|
export declare function classifyFilesystemType(type: number | bigint): FilesystemClassification;
|
|
11
11
|
export interface LocalityDetector {
|
|
12
12
|
readonly platform: string;
|
|
13
|
-
|
|
13
|
+
readonly classify?: (parentPath: string) => FilesystemClassification;
|
|
14
14
|
}
|
|
15
|
+
export declare const linuxLocalityDetector: (statfsType: (parentPath: string) => number | bigint) => LocalityDetector;
|
|
16
|
+
interface DarwinMountEntry {
|
|
17
|
+
readonly mountPoint: string;
|
|
18
|
+
readonly fstype: string;
|
|
19
|
+
readonly flags: ReadonlySet<string>;
|
|
20
|
+
}
|
|
21
|
+
export declare function parseDarwinMountTable(output: string): DarwinMountEntry[];
|
|
22
|
+
export declare function classifyDarwinMount(mountTable: string, resolvedPath: string): FilesystemClassification;
|
|
23
|
+
export declare const darwinLocalityDetector: (readMountTable: () => string) => LocalityDetector;
|
|
15
24
|
export declare function requireLocalDatabasePathWithDetector(databasePath: string, options: {
|
|
16
25
|
assumeLocal?: boolean;
|
|
17
26
|
}, detector: LocalityDetector): string;
|
|
27
|
+
export declare function localityDetectorForPlatform(platform: string): LocalityDetector;
|
|
18
28
|
export declare function requireLocalDatabasePath(databasePath: string, options?: {
|
|
19
29
|
assumeLocal?: boolean;
|
|
20
30
|
}): string;
|
|
31
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { realpathSync, statSync, statfsSync } from 'node:fs';
|
|
2
3
|
import { isAbsolute, dirname } from 'node:path';
|
|
3
4
|
import { DatabasePathError } from './path.js';
|
|
4
5
|
export class LocalityError extends Error {
|
|
@@ -44,6 +45,54 @@ export function classifyFilesystemType(type) {
|
|
|
44
45
|
return { locality: 'local', name: local.name };
|
|
45
46
|
return { locality: 'undetermined', name: `0x${magic.toString(16)}` };
|
|
46
47
|
}
|
|
48
|
+
export const linuxLocalityDetector = (statfsType) => ({
|
|
49
|
+
platform: 'linux',
|
|
50
|
+
classify: parentPath => classifyFilesystemType(statfsType(parentPath)),
|
|
51
|
+
});
|
|
52
|
+
// Darwin filesystem type names that are network transports even when the mount table omits MNT_LOCAL. `local`
|
|
53
|
+
// wins when present (it is the kernel's own verdict); this list only names the refusal, so the operator reads
|
|
54
|
+
// "smbfs" instead of a bare "undetermined".
|
|
55
|
+
const DARWIN_NETWORK_FILESYSTEM_NAMES = new Set(['nfs', 'smbfs', 'afpfs', 'webdav', 'cifs', 'ftp']);
|
|
56
|
+
// `/sbin/mount` prints one `<device> on <mount point> (<fstype>, <flag>, …)` line per mount. The mount point may
|
|
57
|
+
// contain spaces (`/Volumes/My Disk`); the parenthesised list is always last and never contains `)`.
|
|
58
|
+
const DARWIN_MOUNT_LINE = /^.+ on (.+) \(([^)]*)\)$/;
|
|
59
|
+
export function parseDarwinMountTable(output) {
|
|
60
|
+
const entries = [];
|
|
61
|
+
for (const line of output.split('\n')) {
|
|
62
|
+
const match = DARWIN_MOUNT_LINE.exec(line.trim());
|
|
63
|
+
if (!match)
|
|
64
|
+
continue;
|
|
65
|
+
const [fstype = '', ...flags] = match[2].split(',').map(part => part.trim()).filter(Boolean);
|
|
66
|
+
entries.push({ mountPoint: match[1], fstype, flags: new Set(flags) });
|
|
67
|
+
}
|
|
68
|
+
return entries;
|
|
69
|
+
}
|
|
70
|
+
const mountPointCovers = (mountPoint, path) => mountPoint === '/' || path === mountPoint || path.startsWith(mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`);
|
|
71
|
+
export function classifyDarwinMount(mountTable, resolvedPath) {
|
|
72
|
+
let entry;
|
|
73
|
+
for (const candidate of parseDarwinMountTable(mountTable)) {
|
|
74
|
+
if (!mountPointCovers(candidate.mountPoint, resolvedPath))
|
|
75
|
+
continue;
|
|
76
|
+
if (!entry || candidate.mountPoint.length > entry.mountPoint.length)
|
|
77
|
+
entry = candidate;
|
|
78
|
+
}
|
|
79
|
+
if (!entry)
|
|
80
|
+
return { locality: 'undetermined', name: 'no mount table entry' };
|
|
81
|
+
if (entry.flags.has('local'))
|
|
82
|
+
return { locality: 'local', name: entry.fstype };
|
|
83
|
+
if (DARWIN_NETWORK_FILESYSTEM_NAMES.has(entry.fstype))
|
|
84
|
+
return { locality: 'network', name: entry.fstype };
|
|
85
|
+
return { locality: 'undetermined', name: entry.fstype };
|
|
86
|
+
}
|
|
87
|
+
export const darwinLocalityDetector = (readMountTable) => ({
|
|
88
|
+
platform: 'darwin',
|
|
89
|
+
classify: parentPath => {
|
|
90
|
+
// `mount` never fails for a missing directory, so the parent's absence must be raised here for the resolver
|
|
91
|
+
// to keep its actionable PROTOCOL_PATH_PARENT_MISSING error.
|
|
92
|
+
statSync(parentPath);
|
|
93
|
+
return classifyDarwinMount(readMountTable(), realpathSync(parentPath));
|
|
94
|
+
},
|
|
95
|
+
});
|
|
47
96
|
export function requireLocalDatabasePathWithDetector(databasePath, options, detector) {
|
|
48
97
|
if (!isAbsolute(databasePath)) {
|
|
49
98
|
throw new DatabasePathError('PROTOCOL_PATH_NOT_ABSOLUTE', 'databasePath must be absolute before locality detection');
|
|
@@ -51,12 +100,12 @@ export function requireLocalDatabasePathWithDetector(databasePath, options, dete
|
|
|
51
100
|
if (options.assumeLocal)
|
|
52
101
|
return databasePath;
|
|
53
102
|
const parent = dirname(databasePath);
|
|
54
|
-
if (detector.
|
|
103
|
+
if (!detector.classify) {
|
|
55
104
|
throw new LocalityError('LOCALITY_DETECTOR_UNAVAILABLE', `no filesystem locality detector for platform ${detector.platform}; pass --assume-local-storage only after auditing ${parent}`);
|
|
56
105
|
}
|
|
57
|
-
let
|
|
106
|
+
let classification;
|
|
58
107
|
try {
|
|
59
|
-
|
|
108
|
+
classification = detector.classify(parent);
|
|
60
109
|
}
|
|
61
110
|
catch (error) {
|
|
62
111
|
// @@@missing-parent - Preserve the actionable path error without pretending locality was established.
|
|
@@ -65,7 +114,6 @@ export function requireLocalDatabasePathWithDetector(databasePath, options, dete
|
|
|
65
114
|
}
|
|
66
115
|
throw new LocalityError('LOCALITY_PROBE_FAILED', `could not determine the filesystem of ${parent}`, error);
|
|
67
116
|
}
|
|
68
|
-
const classification = classifyFilesystemType(type);
|
|
69
117
|
if (classification.locality === 'network') {
|
|
70
118
|
throw new LocalityError('LOCALITY_NETWORK_FILESYSTEM', `${parent} is ${classification.name}; advisory locking is not admitted there`);
|
|
71
119
|
}
|
|
@@ -74,10 +122,14 @@ export function requireLocalDatabasePathWithDetector(databasePath, options, dete
|
|
|
74
122
|
}
|
|
75
123
|
return databasePath;
|
|
76
124
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
125
|
+
const readDarwinMountTable = () => execFileSync('/sbin/mount', [], { encoding: 'utf8', timeout: 5_000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
126
|
+
export function localityDetectorForPlatform(platform) {
|
|
127
|
+
if (platform === 'linux')
|
|
128
|
+
return linuxLocalityDetector(parentPath => statfsSync(parentPath).type);
|
|
129
|
+
if (platform === 'darwin')
|
|
130
|
+
return darwinLocalityDetector(readDarwinMountTable);
|
|
131
|
+
return { platform };
|
|
132
|
+
}
|
|
81
133
|
export function requireLocalDatabasePath(databasePath, options = {}) {
|
|
82
|
-
return requireLocalDatabasePathWithDetector(databasePath, options,
|
|
134
|
+
return requireLocalDatabasePathWithDetector(databasePath, options, localityDetectorForPlatform(process.platform));
|
|
83
135
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-selflaunch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-next.0",
|
|
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.
|
|
28
|
-
"@spexcode/session-runtime": "0.
|
|
27
|
+
"@spexcode/session-protocol": "0.7.0-next.0",
|
|
28
|
+
"@spexcode/session-runtime": "0.7.0-next.0"
|
|
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.
|
|
3
|
+
"version": "0.7.0-next.0",
|
|
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.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.0" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// @@@ spex launcher ([[release-launcher]]) - package installs execute the package's compiled CLI directly;
|
|
3
3
|
// tsx remains a development tool and never enters an adopter's runtime closure.
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process'
|
|
5
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync } from 'node:fs'
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
6
7
|
import { fileURLToPath } from 'node:url'
|
|
7
8
|
import { dirname, join } from 'node:path'
|
|
8
9
|
|
|
@@ -16,7 +17,19 @@ const workspace = join(pkg, '..')
|
|
|
16
17
|
// carry unresolved conflict markers, so hooks keep the retryable exit-75 contract during a merge.
|
|
17
18
|
const sourceRoot = join(pkg, 'src')
|
|
18
19
|
if (existsSync(sourceRoot)) {
|
|
19
|
-
const
|
|
20
|
+
const buildTargets = [
|
|
21
|
+
['packages/session-protocol', 'dist/index.js'],
|
|
22
|
+
['packages/session-topology', 'dist/index.js'],
|
|
23
|
+
['packages/session-runtime', 'dist/index.js'],
|
|
24
|
+
['packages/session-events', 'dist/index.js'],
|
|
25
|
+
['packages/session-application', 'dist/index.js'],
|
|
26
|
+
['packages/session-selflaunch', 'dist/index.js'],
|
|
27
|
+
['packages/spec-core', 'dist/index.js'],
|
|
28
|
+
['spec-eval', 'dist/index.js'],
|
|
29
|
+
['spec-forge', 'dist/index.js'],
|
|
30
|
+
['spec-cli', 'dist/cli.js'],
|
|
31
|
+
]
|
|
32
|
+
const srcRoots = buildTargets.map(([packagePath]) => join(pkg, '..', packagePath, 'src'))
|
|
20
33
|
const conflicted = srcRoots.flatMap((root) => {
|
|
21
34
|
if (!existsSync(root)) return []
|
|
22
35
|
return readdirSync(root, { recursive: true })
|
|
@@ -34,8 +47,10 @@ if (existsSync(sourceRoot)) {
|
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
// @@@ source-workspace build ([[release-launcher]]) - the git hooks invoke this launcher straight from a
|
|
37
|
-
// checkout, where dist is intentionally untracked.
|
|
38
|
-
//
|
|
50
|
+
// checkout, where dist is intentionally untracked. Import main's fresh closure and rebuild only changed
|
|
51
|
+
// packages plus dependents before linting candidate source; an untrusted baseline falls back to the complete
|
|
52
|
+
// ordered driver. Every package still publishes its dist atomically. An installed package has no src tree
|
|
53
|
+
// and stays a pure Node -> dist execution path.
|
|
39
54
|
const runtimeEntries = [
|
|
40
55
|
cli,
|
|
41
56
|
join(workspace, 'packages', 'spec-core', 'dist', 'index.js'),
|
|
@@ -49,8 +64,9 @@ if (existsSync(sourceRoot)) {
|
|
|
49
64
|
&& !/\.d\.ts$/.test(path)
|
|
50
65
|
&& !/(^|[./])[^/]+\.test\.(ts|tsx|js|mjs)$/.test(path)
|
|
51
66
|
}
|
|
52
|
-
const
|
|
53
|
-
const
|
|
67
|
+
const newestRuntimeSource = (root) => {
|
|
68
|
+
const roots = buildTargets.map(([packagePath]) => join(root, packagePath, 'src'))
|
|
69
|
+
return roots.reduce((newest, root) => {
|
|
54
70
|
if (!existsSync(root)) return newest
|
|
55
71
|
for (const entry of readdirSync(root, { recursive: true })) {
|
|
56
72
|
const path = join(root, String(entry))
|
|
@@ -60,6 +76,94 @@ if (existsSync(sourceRoot)) {
|
|
|
60
76
|
}
|
|
61
77
|
return newest
|
|
62
78
|
}, 0)
|
|
79
|
+
}
|
|
80
|
+
const newestPackageSource = (root, packagePath) => {
|
|
81
|
+
const source = join(root, packagePath, 'src')
|
|
82
|
+
if (!existsSync(source)) return 0
|
|
83
|
+
let newest = 0
|
|
84
|
+
const visit = (dir) => {
|
|
85
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
86
|
+
const path = join(dir, entry.name)
|
|
87
|
+
if (entry.isDirectory()) visit(path)
|
|
88
|
+
else if (isRuntimeSource(path)) {
|
|
89
|
+
try { newest = Math.max(newest, statSync(path).mtimeMs) } catch { /* retry on the next launch */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
visit(source)
|
|
94
|
+
return newest
|
|
95
|
+
}
|
|
96
|
+
const entriesAreFresh = (root) => {
|
|
97
|
+
const newestSource = newestRuntimeSource(root)
|
|
98
|
+
return buildTargets.every(([packagePath, entry]) => {
|
|
99
|
+
try { return statSync(join(root, packagePath, entry)).mtimeMs >= newestSource } catch { return false }
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
const packageFingerprint = (root, packagePath) => {
|
|
103
|
+
const hash = createHash('sha256')
|
|
104
|
+
const packageRoot = join(root, packagePath)
|
|
105
|
+
const files = ['package.json', 'tsconfig.build.json'].map((name) => join(packageRoot, name)).filter(existsSync)
|
|
106
|
+
const visit = (dir, relative = '') => {
|
|
107
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
108
|
+
const path = join(dir, entry.name)
|
|
109
|
+
const child = relative ? `${relative}/${entry.name}` : entry.name
|
|
110
|
+
if (entry.isDirectory()) visit(path, child)
|
|
111
|
+
else if (isRuntimeSource(path)) files.push(path)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const source = join(packageRoot, 'src')
|
|
115
|
+
if (existsSync(source)) visit(source)
|
|
116
|
+
files.sort()
|
|
117
|
+
for (const path of files) hash.update(path.slice(packageRoot.length)).update('\0').update(readFileSync(path)).update('\0')
|
|
118
|
+
return hash.digest('hex')
|
|
119
|
+
}
|
|
120
|
+
const packageDependents = {
|
|
121
|
+
'packages/session-protocol': ['packages/session-topology', 'packages/session-runtime', 'packages/session-events', 'packages/session-application', 'packages/session-selflaunch', 'spec-cli'],
|
|
122
|
+
'packages/session-topology': ['packages/session-application', 'spec-cli'],
|
|
123
|
+
'packages/session-runtime': ['packages/session-application', 'packages/session-selflaunch', 'spec-cli'],
|
|
124
|
+
'packages/session-events': ['packages/session-application', 'spec-cli'],
|
|
125
|
+
'packages/session-application': ['spec-cli'],
|
|
126
|
+
'packages/session-selflaunch': ['spec-cli'],
|
|
127
|
+
'packages/spec-core': ['spec-eval', 'spec-forge', 'spec-cli'],
|
|
128
|
+
'spec-eval': ['spec-cli'],
|
|
129
|
+
'spec-forge': ['spec-cli'],
|
|
130
|
+
'spec-cli': [],
|
|
131
|
+
}
|
|
132
|
+
const changedBuildPackages = (root) => {
|
|
133
|
+
const changed = new Set(buildTargets
|
|
134
|
+
.map(([packagePath]) => packagePath)
|
|
135
|
+
.filter((packagePath) => packageFingerprint(workspace, packagePath) !== packageFingerprint(root, packagePath)))
|
|
136
|
+
for (const [packagePath, entry] of buildTargets) {
|
|
137
|
+
try {
|
|
138
|
+
if (statSync(join(root, packagePath, entry)).mtimeMs < newestPackageSource(workspace, packagePath)) changed.add(packagePath)
|
|
139
|
+
} catch { changed.add(packagePath) }
|
|
140
|
+
}
|
|
141
|
+
for (const packagePath of [...changed]) for (const dependent of packageDependents[packagePath]) changed.add(dependent)
|
|
142
|
+
return buildTargets.map(([packagePath]) => packagePath).filter((packagePath) => changed.has(packagePath))
|
|
143
|
+
}
|
|
144
|
+
const mainRoot = (() => {
|
|
145
|
+
const result = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd: workspace, encoding: 'utf8' })
|
|
146
|
+
if (result.status !== 0) return null
|
|
147
|
+
const commonDir = result.stdout.trim()
|
|
148
|
+
return commonDir ? dirname(commonDir) : null
|
|
149
|
+
})()
|
|
150
|
+
const reusableMainBuild = () => mainRoot && mainRoot !== workspace && entriesAreFresh(mainRoot)
|
|
151
|
+
const importMainBuild = () => {
|
|
152
|
+
for (const [packagePath] of buildTargets) {
|
|
153
|
+
const source = join(mainRoot, packagePath, 'dist')
|
|
154
|
+
const target = join(workspace, packagePath, 'dist')
|
|
155
|
+
const temporary = `${target}.from-main-${process.pid}`
|
|
156
|
+
const previous = `${target}.from-main-previous-${process.pid}`
|
|
157
|
+
rmSync(temporary, { recursive: true, force: true })
|
|
158
|
+
rmSync(previous, { recursive: true, force: true })
|
|
159
|
+
cpSync(source, temporary, { recursive: true })
|
|
160
|
+
if (existsSync(target)) renameSync(target, previous)
|
|
161
|
+
renameSync(temporary, target)
|
|
162
|
+
rmSync(previous, { recursive: true, force: true })
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const sourceIsStale = () => {
|
|
166
|
+
const newestSource = newestRuntimeSource(workspace)
|
|
63
167
|
return runtimeEntries.some((entry) => {
|
|
64
168
|
try { return statSync(entry).mtimeMs < newestSource } catch { return true }
|
|
65
169
|
})
|
|
@@ -90,10 +194,23 @@ if (existsSync(sourceRoot)) {
|
|
|
90
194
|
}
|
|
91
195
|
try {
|
|
92
196
|
if (sourceIsStale()) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
console.error(
|
|
96
|
-
|
|
197
|
+
if (reusableMainBuild()) {
|
|
198
|
+
const changed = changedBuildPackages(mainRoot)
|
|
199
|
+
console.error(`spex: importing fresh runtime closure from main; rebuilding ${changed.length} package(s)`)
|
|
200
|
+
importMainBuild()
|
|
201
|
+
if (changed.length) {
|
|
202
|
+
const build = spawnSync(process.execPath, [join(workspace, 'scripts', 'build-workspaces.mjs'), ...changed], { cwd: workspace, stdio: 'inherit' })
|
|
203
|
+
if (build.error || build.status !== 0 || !existsSync(cli)) {
|
|
204
|
+
console.error('spex: source workspace selective build failed; fix it, then retry.')
|
|
205
|
+
process.exit(build.status ?? 1)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
const build = spawnSync('npm', ['run', 'build'], { cwd: workspace, stdio: 'inherit' })
|
|
210
|
+
if (build.error || build.status !== 0 || !existsSync(cli)) {
|
|
211
|
+
console.error('spex: source workspace build failed; fix it, then retry.')
|
|
212
|
+
process.exit(build.status ?? 1)
|
|
213
|
+
}
|
|
97
214
|
}
|
|
98
215
|
}
|
|
99
216
|
} finally {
|
|
@@ -1831,6 +1831,25 @@ else if (cmd === 'internal') {
|
|
|
1831
1831
|
process.exit(1);
|
|
1832
1832
|
}
|
|
1833
1833
|
}
|
|
1834
|
+
else if (sub === 'codex-reopen') {
|
|
1835
|
+
// A headless resume reloads its evicted thread into the shared app-server (thread/resume, no turn), so
|
|
1836
|
+
// readiness — which proves online only for a RESIDENT thread — can see it. The visible TUI does this
|
|
1837
|
+
// implicitly by attaching; headless has no TUI, so its launch.sh resume branch calls this.
|
|
1838
|
+
const { codexReopenThread } = await import('./harness.js');
|
|
1839
|
+
const sock = process.argv[4], tid = process.argv[5];
|
|
1840
|
+
if (!sock || !tid) {
|
|
1841
|
+
console.error('usage: spex internal codex-reopen <sock> <threadId>');
|
|
1842
|
+
process.exit(2);
|
|
1843
|
+
}
|
|
1844
|
+
const r = await codexReopenThread(sock, tid);
|
|
1845
|
+
if (r.ok) {
|
|
1846
|
+
console.log('reopened');
|
|
1847
|
+
}
|
|
1848
|
+
else {
|
|
1849
|
+
console.error(r.error);
|
|
1850
|
+
process.exit(1);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1834
1853
|
else if (sub === 'check-staged') {
|
|
1835
1854
|
// the pre-commit hook's eval backstop: a staged stray evidence blob or malformed eval.md rejects the
|
|
1836
1855
|
// commit. Logic lives in spec-eval; the hook shims here.
|
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
import { copyFileSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, copyFileSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
// A differing target is replaced by rename, never rewritten in place: a concurrent reader — codex re-reading its
|
|
4
|
+
// own config.toml, a harness loading its hooks file — sees the old bytes or the new bytes, never a truncated
|
|
5
|
+
// middle. The replacement lands on the real file behind a symlink and keeps that file's mode.
|
|
2
6
|
export function writeFileIfChanged(path, content) {
|
|
3
7
|
const next = Buffer.from(content);
|
|
8
|
+
let target = path;
|
|
9
|
+
let mode;
|
|
4
10
|
try {
|
|
5
11
|
if (readFileSync(path).equals(next))
|
|
6
12
|
return false;
|
|
13
|
+
target = realpathSync(path);
|
|
14
|
+
mode = statSync(target).mode;
|
|
7
15
|
}
|
|
8
16
|
catch (error) {
|
|
9
17
|
if (error?.code !== 'ENOENT')
|
|
10
18
|
throw error;
|
|
11
19
|
}
|
|
12
|
-
|
|
20
|
+
const staged = join(dirname(target), `.${basename(target)}.${process.pid}.tmp`);
|
|
21
|
+
writeFileSync(staged, next);
|
|
22
|
+
if (mode !== undefined)
|
|
23
|
+
chmodSync(staged, mode);
|
|
24
|
+
renameSync(staged, target);
|
|
13
25
|
return true;
|
|
14
26
|
}
|
|
15
27
|
export function copyFileIfChanged(source, target) {
|
|
@@ -307,7 +307,7 @@ PORTABILITY, and picking the right one is the whole discipline:
|
|
|
307
307
|
below); a targeted env override (SPEXCODE_CODEX_SERVER_CMD, …) still wins at its read site.
|
|
308
308
|
|
|
309
309
|
Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A branch name, a dashboard
|
|
310
|
-
icon
|
|
310
|
+
icon, upload policy, lint policy, resource and doctor health budgets, and a launcher's name+harness are project facts → committed spexcode.json. The ABSOLUTE
|
|
311
311
|
PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
|
|
312
312
|
Both files are optional; omit any field to take its default, except \`sessions.defaultLauncher\` when using
|
|
313
313
|
\`spex session new\` or the dashboard without an explicit launcher choice.
|
|
@@ -339,13 +339,8 @@ Example — a repo whose trunk is \`staging\`, not \`main\`:
|
|
|
339
339
|
dashboard.apiUrl the per-project backend the dashboard proxies to (read frontend-side). For a SHARED
|
|
340
340
|
install prefer the API_URL env var; apiUrl here is the default only when the dashboard
|
|
341
341
|
lives inside the project.
|
|
342
|
-
dashboard.showHeadlessLaunchers
|
|
343
|
-
include launchers whose harness declares itself headless in the dashboard New Session
|
|
344
|
-
picker. Default: false. This changes dashboard visibility only; explicit CLI
|
|
345
|
-
--launcher selection can still use every configured launcher.
|
|
346
342
|
Example:
|
|
347
|
-
{ "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch"
|
|
348
|
-
"showHeadlessLaunchers": false } }
|
|
343
|
+
{ "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch" } }
|
|
349
344
|
|
|
350
345
|
── HOST GATEWAY ($SPEXCODE_HOME/config.json — per-user host identity, never a project file) ──
|
|
351
346
|
gateway.icon the global /projects icon, using the same preset ids above. Default: "gateway".
|
|
@@ -403,14 +398,17 @@ A named launcher profile fixes BOTH a session's harness AND its exact launch com
|
|
|
403
398
|
by name with --launcher/the dashboard dropdown, and the chosen name is persisted on the record so a resume
|
|
404
399
|
reuses the same auth. There are NO magic built-ins: \`spex init\` SEEDS an ordinary named launcher for each
|
|
405
400
|
harness the adopter SELECTED (--harness), from the template pool
|
|
406
|
-
"claude" → { "harness": "claude", "cmd": "claude" }
|
|
407
401
|
"claude-headless" → { "harness": "claude-headless", "cmd": "claude" }
|
|
408
|
-
"
|
|
402
|
+
"claude" → { "harness": "claude", "cmd": "claude" }
|
|
409
403
|
"codex-headless" → { "harness": "codex-headless", "cmd": "codex --yolo" }
|
|
410
|
-
"
|
|
404
|
+
"codex" → { "harness": "codex", "cmd": "codex" }
|
|
411
405
|
"opencode-headless" → { "harness": "opencode-headless", "cmd": "opencode --auto" }
|
|
412
|
-
"
|
|
406
|
+
"opencode" → { "harness": "opencode", "cmd": "opencode" }
|
|
413
407
|
"pi-headless" → { "harness": "pi-headless", "cmd": "pi" }
|
|
408
|
+
"pi" → { "harness": "pi", "cmd": "pi" }
|
|
409
|
+
Each harness's headless (terminal-free) form leads its interactive one, and \`spex init\` makes the FIRST
|
|
410
|
+
planted launcher the default — so an adoption that selects both forms of a harness creates its sessions
|
|
411
|
+
headless by default, read through the dashboard's Conversation; the interactive TUI launcher is the opt-in.
|
|
414
412
|
The interactive profiles preserve each harness's normal permission model. \`opencode-headless\` and
|
|
415
413
|
\`codex-headless\` are deliberate seed exceptions: their terminal-free runs require \`opencode --auto\` and
|
|
416
414
|
\`codex --yolo\`; interactive profiles stay plain. Other automatic-permission commands are NEVER clean-init
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type SlashCommand } from './slash-commands.js';
|
|
2
|
-
import { type
|
|
3
|
-
import { type TranscriptRead, type TranscriptRange } from './transcript-reader.js';
|
|
2
|
+
import { type TranscriptReader } from '@spexcode/transcript';
|
|
4
3
|
import { type HarnessId } from '@spexcode/spec-core';
|
|
5
4
|
export type { HarnessId } from '@spexcode/spec-core';
|
|
6
5
|
export type HarnessLivenessRecord = {
|
|
@@ -123,8 +122,7 @@ export interface Harness {
|
|
|
123
122
|
readonly events: readonly string[];
|
|
124
123
|
readonly ownsRendezvous: boolean;
|
|
125
124
|
readonly paneTitleIsSelfSummary: boolean;
|
|
126
|
-
|
|
127
|
-
readTranscript(threadId: string, range: TranscriptRange): Promise<TranscriptRead>;
|
|
125
|
+
readonly transcript: TranscriptReader;
|
|
128
126
|
launchCmd(id: string, runtimeDir?: string, cmd?: string): string;
|
|
129
127
|
baseCmd(cmd?: string): string;
|
|
130
128
|
oneShotTurn?(prompt: string, cmd?: string): {
|
|
@@ -234,6 +232,9 @@ export declare const codexAppServerReceipt: (dir?: string) => string;
|
|
|
234
232
|
export declare function deliverViaRendezvous(id: string, text: string, mid?: string, wallMs?: number): Promise<DispatchResult>;
|
|
235
233
|
export declare function deliverViaClaudeRendezvous(id: string, text: string, mid?: string, runtimeDir?: string): Promise<DispatchResult>;
|
|
236
234
|
export declare function deliverViaSocketOrWake(id: string, text: string, mid: string | undefined, coldWake: () => Promise<DispatchResult>, unprovenError: string): Promise<DispatchResult>;
|
|
235
|
+
export declare function interruptViaRendezvous(id: string, harness: string, opts?: {
|
|
236
|
+
settle?: boolean;
|
|
237
|
+
}): Promise<DispatchResult>;
|
|
237
238
|
type JsonRpc = {
|
|
238
239
|
id?: number;
|
|
239
240
|
method?: string;
|
|
@@ -261,6 +262,12 @@ export declare function sessionIdentityEnvVars(): string[];
|
|
|
261
262
|
export declare function codexLaunchCommand(id: string, codexCmd?: string, serverCmd?: string, dir?: string, attachTui?: boolean): string;
|
|
262
263
|
export declare const CODEX_TURN_OBSERVER_SUBSCRIBE_MS = 30000;
|
|
263
264
|
export declare function codexTurnFailureObserver(rec: HarnessDeliveryRecord, onFailure: (failure: TurnFailure) => void): FailureSubscription;
|
|
265
|
+
export declare function codexReopenThread(sock: string, threadId: string, budgetMs?: number): Promise<{
|
|
266
|
+
ok: true;
|
|
267
|
+
} | {
|
|
268
|
+
ok: false;
|
|
269
|
+
error: string;
|
|
270
|
+
}>;
|
|
264
271
|
export declare const CODEX_THREAD_SOURCE_KINDS: readonly ["cli", "vscode", "exec", "appServer", "subAgent", "subAgentReview", "subAgentCompact", "subAgentThreadSpawn", "subAgentOther", "unknown"];
|
|
265
272
|
export declare function codexLoadedReferenceIds(sock: string): Promise<{
|
|
266
273
|
ok: true;
|
|
@@ -343,7 +350,6 @@ export type LauncherDefault = {
|
|
|
343
350
|
error: string | null;
|
|
344
351
|
};
|
|
345
352
|
export declare function launcherList(root?: string): Launcher[];
|
|
346
|
-
export declare function dashboardLauncherList(root?: string): Launcher[];
|
|
347
353
|
export declare const MISSING_DEFAULT_LAUNCHER_ERROR = "sessions.defaultLauncher is required for a launch without --launcher; set it in spexcode.json or spexcode.local.json (for example {\"sessions\":{\"defaultLauncher\":\"claude\"}})";
|
|
348
354
|
export declare function defaultLauncher(root?: string): string;
|
|
349
355
|
export declare function launcherDefault(root?: string): LauncherDefault;
|