@spexcode/spec-cli 0.6.7 → 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/bin/spex.mjs +175 -21
- package/dist/cli.js +121 -59
- package/dist/client.d.ts +1 -3
- package/dist/client.js +49 -30
- package/dist/codex-runtime-generations.d.ts +11 -0
- package/dist/codex-runtime-generations.js +46 -9
- package/dist/delivery-lock.d.ts +2 -0
- package/dist/delivery-lock.js +58 -0
- package/dist/doctor.js +53 -11
- package/dist/file-write.js +14 -2
- package/dist/gateway-hub.js +2 -1
- package/dist/gateway.js +6 -3
- package/dist/graphCache.js +32 -2
- package/dist/graphSnapshot.js +57 -2
- package/dist/graphStream.d.ts +2 -0
- package/dist/graphStream.js +83 -3
- package/dist/guide.js +29 -18
- package/dist/harness-select.js +16 -3
- package/dist/harness.d.ts +24 -6
- package/dist/harness.js +510 -72
- package/dist/help.js +11 -8
- package/dist/hook-prompts.js +8 -0
- package/dist/host-resources.js +29 -8
- package/dist/host.d.ts +7 -0
- package/dist/host.js +98 -1
- package/dist/index.js +294 -36
- package/dist/init.js +1 -1
- package/dist/lint.js +70 -35
- package/dist/listen.d.ts +3 -2
- package/dist/listen.js +14 -2
- package/dist/machine-peer.d.ts +2 -1
- package/dist/machine-peer.js +46 -15
- package/dist/materialize.d.ts +2 -2
- package/dist/materialize.js +176 -35
- package/dist/opencode.js +10 -2
- package/dist/pi-harness.js +18 -5
- package/dist/pi-headless.d.ts +2 -0
- package/dist/pi-headless.js +27 -0
- package/dist/pty-bridge.js +14 -14
- package/dist/reviews.js +12 -7
- package/dist/runtime-ownership.d.ts +11 -0
- package/dist/runtime-ownership.js +79 -1
- package/dist/session-application.d.ts +23 -0
- package/dist/session-application.js +189 -0
- package/dist/session-declarations.js +13 -1
- package/dist/session-files.d.ts +6 -0
- package/dist/session-files.js +13 -1
- package/dist/session-follow.js +39 -22
- package/dist/session-record-lock.d.ts +3 -0
- package/dist/session-record-lock.js +94 -0
- package/dist/session-runtime-adapter.d.ts +44 -0
- package/dist/session-runtime-adapter.js +37 -0
- package/dist/session-timeline.d.ts +25 -2
- package/dist/session-timeline.js +68 -11
- package/dist/session-transcript.d.ts +57 -0
- package/dist/session-transcript.js +137 -0
- package/dist/session-web.js +4 -4
- package/dist/sessions.d.ts +112 -15
- package/dist/sessions.js +1553 -764
- package/dist/shim-runtime.js +14 -0
- package/dist/source-list.d.ts +13 -0
- package/dist/source-list.js +99 -0
- package/dist/source-read.d.ts +16 -0
- package/dist/source-read.js +84 -0
- package/dist/spec-attachments.d.ts +7 -0
- package/dist/spec-attachments.js +89 -0
- package/dist/spec-body-edit.d.ts +23 -0
- package/dist/spec-body-edit.js +138 -0
- package/dist/supervise.js +15 -6
- package/hooks/dispatch.sh +19 -31
- package/hooks/harness.sh +6 -6
- package/package.json +8 -6
- package/templates/hooks/post-checkout +4 -2
- package/templates/hooks/post-merge +2 -1
- package/templates/hooks/pre-commit +5 -3
- package/templates/hooks/reference-transaction +5 -3
- package/templates/spec/project/.plugins/commands/spec.md +2 -7
- package/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
- package/templates/spec/project/.plugins/core/idle/spec.md +1 -1
- package/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
- package/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
- package/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
- package/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
- package/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
- package/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
- package/templates/spec/project/.plugins/core/spec.md +2 -0
- package/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
- package/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
- package/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
- package/templates/spec/project/.plugins/skills/spec.md +2 -6
- package/templates/spec/project/.plugins/spec.md +7 -0
- package/dist/execution-trace.d.ts +0 -26
- package/dist/execution-trace.js +0 -475
- package/dist/session-execution.d.ts +0 -10
- package/dist/session-execution.js +0 -70
- package/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
- package/hooks/compat/mark-active-sed-v0.fixture +0 -46
package/dist/graphStream.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export type PendingGraphChanges = {
|
|
|
58
58
|
export declare const addPendingGraphChange: (pending: PendingGraphChanges, scope: Scope) => PendingGraphChanges;
|
|
59
59
|
export declare function isSessionCreateCandidateRegistryEvent(relativePath: string, candidatePaths: Iterable<string>): boolean;
|
|
60
60
|
export declare const notifyBoardChanged: (scope?: Scope) => void;
|
|
61
|
+
export declare const sessionDatabaseWatchIgnore: (databasePath: string) => ((relativePath: string) => boolean);
|
|
62
|
+
export declare function watchSessionDatabase(databasePath: string, onInput: () => void, onFailure: (error: Error) => void, watchFactory?: WatchFactory): TreeWatcherRegistry;
|
|
61
63
|
type RegistryGroup = {
|
|
62
64
|
root: string;
|
|
63
65
|
close(): void;
|
package/dist/graphStream.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { streamSSE } from 'hono/streaming';
|
|
2
2
|
import { watch, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join, dirname, relative, resolve, basename } from 'node:path';
|
|
4
|
-
import { sessionsRoot, gitCommonDir, repoRoot, sessionBranchIndex, mainBranch } from '@spexcode/spec-core';
|
|
4
|
+
import { sessionsRoot, gitCommonDir, repoRoot, sessionBranchIndex, mainBranch, isTrashWorktreePath } from '@spexcode/spec-core';
|
|
5
|
+
import { resolveDatabasePath } from '@spexcode/session-selflaunch';
|
|
5
6
|
import { hotSignature, warmSignature, listSessions, pendingSessionCreateWorktreePaths } from './sessions.js';
|
|
6
7
|
import { getBoard, getBoardForSessionRefresh, invalidateBoard, patrolBoard } from './graphCache.js';
|
|
7
8
|
import { unitize, tagOf, diffUnits } from '@spexcode/spec-core';
|
|
@@ -439,7 +440,7 @@ function fireChanged(scope = 'full', evalTarget) {
|
|
|
439
440
|
}
|
|
440
441
|
// ---- event source 0: an EXPLICIT server-side nudge ----
|
|
441
442
|
// for a server-side mutation that must show instantly regardless of watcher health: /rename writes the
|
|
442
|
-
// session's global
|
|
443
|
+
// session's global runtime envelope (`runtime.json` — [[session-rename]]), which lives INSIDE the watched store, so
|
|
443
444
|
// source 1 normally sees the write too. The explicit route call stays because that fs watch is best-effort
|
|
444
445
|
// (it can fail to attach), and the nudge makes the sub-second rename guarantee deterministic. Same
|
|
445
446
|
// debounced funnel as every other source; defaults to 'full' but the rename route passes 'sessions'.
|
|
@@ -535,6 +536,80 @@ function ensureWatcher(root) {
|
|
|
535
536
|
}
|
|
536
537
|
noteSourceHealthy('store');
|
|
537
538
|
}
|
|
539
|
+
// ---- event source 1b: the canonical session database (lifecycle commits from ANY process) → 'sessions' ----
|
|
540
|
+
// Since the JSON cutover ([[production-cutin]]) a lifecycle transition is a SQLite commit, not a write inside the
|
|
541
|
+
// store above: that watch still sees the runtime envelope and the prompt, but the state a HOOK authors — mark-active
|
|
542
|
+
// on every prompt and tool call, the stop-gate's declarations, idle — is committed by the hook's OWN process through
|
|
543
|
+
// `spex internal session-state`. The backend's in-process commit observer (index.ts) bridges only its own commits,
|
|
544
|
+
// so without this leaf a hook-authored flip reached the board only when some unrelated signal happened to re-splice:
|
|
545
|
+
// a message sent from the dashboard left the row idle for minutes (measured on the dogfood board: 150s of nothing
|
|
546
|
+
// but pings after the commit). One NON-recursive watch on the database's directory, delivering only the database's
|
|
547
|
+
// own names — the file and its `-journal` (journal_mode=delete writes both on every commit); every other file in
|
|
548
|
+
// that directory is filtered out at delivery. Attach failure is held and repaired like every other source, and
|
|
549
|
+
// [[graph-cache]] folds the same file into its session revision so the patrol covers a held or disabled leaf.
|
|
550
|
+
let sessionDatabaseWatcher = null;
|
|
551
|
+
let activeDatabasePath = null;
|
|
552
|
+
const SESSION_DB_SOURCE = 'session-db';
|
|
553
|
+
export const sessionDatabaseWatchIgnore = (databasePath) => {
|
|
554
|
+
const name = basename(databasePath);
|
|
555
|
+
return (relativePath) => relativePath !== name && !relativePath.startsWith(`${name}-`);
|
|
556
|
+
};
|
|
557
|
+
export function watchSessionDatabase(databasePath, onInput, onFailure, watchFactory) {
|
|
558
|
+
return new TreeWatcherRegistry({
|
|
559
|
+
root: dirname(databasePath),
|
|
560
|
+
source: SESSION_DB_SOURCE,
|
|
561
|
+
scope: 'sessions',
|
|
562
|
+
recursive: false,
|
|
563
|
+
ignore: sessionDatabaseWatchIgnore(databasePath),
|
|
564
|
+
watchFactory,
|
|
565
|
+
onInput: () => onInput(),
|
|
566
|
+
onFailure,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
function closeSessionDatabaseWatcher() {
|
|
570
|
+
sessionDatabaseWatcher?.close();
|
|
571
|
+
sessionDatabaseWatcher = null;
|
|
572
|
+
activeDatabasePath = null;
|
|
573
|
+
}
|
|
574
|
+
function ensureSessionDatabaseWatcher() {
|
|
575
|
+
if (isDisabled(SESSION_DB_SOURCE)) {
|
|
576
|
+
closeSessionDatabaseWatcher();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
let databasePath;
|
|
580
|
+
try {
|
|
581
|
+
databasePath = resolveDatabasePath();
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
noteSourceFailure(SESSION_DB_SOURCE, error);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (sessionDatabaseWatcher && activeDatabasePath === databasePath)
|
|
588
|
+
return;
|
|
589
|
+
closeSessionDatabaseWatcher();
|
|
590
|
+
activeDatabasePath = databasePath;
|
|
591
|
+
if (!mayAttach(SESSION_DB_SOURCE))
|
|
592
|
+
return;
|
|
593
|
+
try {
|
|
594
|
+
mkdirSync(dirname(databasePath), { recursive: true });
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
console.error(`spec-cli: graph watcher '${SESSION_DB_SOURCE}' could not create ${dirname(databasePath)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
598
|
+
}
|
|
599
|
+
const registry = watchSessionDatabase(databasePath, () => fireChanged('sessions'), (error) => {
|
|
600
|
+
if (sessionDatabaseWatcher === registry)
|
|
601
|
+
sessionDatabaseWatcher = null;
|
|
602
|
+
noteSourceFailure(SESSION_DB_SOURCE, error);
|
|
603
|
+
fireChanged('sessions');
|
|
604
|
+
});
|
|
605
|
+
sessionDatabaseWatcher = registry;
|
|
606
|
+
if (!registry.refresh()) {
|
|
607
|
+
if (sessionDatabaseWatcher === registry)
|
|
608
|
+
sessionDatabaseWatcher = null;
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
noteSourceHealthy(SESSION_DB_SOURCE);
|
|
612
|
+
}
|
|
538
613
|
let refsWatchers = null;
|
|
539
614
|
const REFS_OBSERVER = 'graph:refs';
|
|
540
615
|
// @@@ the moved ref NAMES its scope - the watcher has always known which ref moved and threw it away, so
|
|
@@ -657,13 +732,14 @@ const worktreeSource = (name) => `worktree:${name}`;
|
|
|
657
732
|
const PROJECT_ROOT_SOURCE = 'project-root';
|
|
658
733
|
let projectRootWatcher = null;
|
|
659
734
|
const ignoredWorktreePath = (file) => file.split(/[\\/]/).some((segment) => segment === '.git' || segment === 'node_modules');
|
|
735
|
+
const ignoredGeneratedBuildPath = (file) => file.split(/[\\/]/).some((segment) => segment === 'dist' || segment.startsWith('.dist-next-') || segment.startsWith('.dist-previous-'));
|
|
660
736
|
// @@@ linked worktrees are not graph input for THIS backend - the board's node statuses derive from the
|
|
661
737
|
// served checkout's own HEAD, so a file under `.worktrees/<node>` belongs to a different branch's tree and
|
|
662
738
|
// cannot move any status here until it lands and this HEAD advances. Watching them registers one inotify
|
|
663
739
|
// watch per directory (Linux takes the exact-directory transport) and buys nothing: measured on this repo,
|
|
664
740
|
// 20,124 of 20,473 watched directories were linked worktrees against 843 in the served tree.
|
|
665
741
|
// The per-worktree registries above keep their own roots; only the project-root sweep skips them.
|
|
666
|
-
export const ignoredProjectRootPath = (file) => ignoredWorktreePath(file) || file.split(/[\\/]/).some((segment) => segment === '.worktrees');
|
|
742
|
+
export const ignoredProjectRootPath = (file) => ignoredWorktreePath(file) || ignoredGeneratedBuildPath(file) || file.split(/[\\/]/).some((segment) => segment === '.worktrees');
|
|
667
743
|
// The directory whose tree this backend serves is graph input even before it has a `.spec` tree or any live
|
|
668
744
|
// session worktree. Keeping it in the same root registry as linked worktrees means a first `spex init` or
|
|
669
745
|
// agent-created spec invalidates a warmed empty board instead of leaving a confidently stale cache until a
|
|
@@ -807,6 +883,8 @@ async function reconcileWorktreePass(forcedSessions, era, common) {
|
|
|
807
883
|
catch {
|
|
808
884
|
continue;
|
|
809
885
|
}
|
|
886
|
+
if (isTrashWorktreePath(wtPath))
|
|
887
|
+
continue;
|
|
810
888
|
const normalizedPath = resolve(wtPath);
|
|
811
889
|
if (!wantedPaths.has(normalizedPath)) {
|
|
812
890
|
if (dropWorktreeWatcher(e.name))
|
|
@@ -1033,6 +1111,7 @@ export async function ensureBoardFileWatchers(forceSessionId) {
|
|
|
1033
1111
|
activeStoreRoot = storeRoot;
|
|
1034
1112
|
activeCommonRoot = commonRoot;
|
|
1035
1113
|
ensureWatcher(storeRoot);
|
|
1114
|
+
ensureSessionDatabaseWatcher();
|
|
1036
1115
|
ensureRefsWatcher(commonRoot);
|
|
1037
1116
|
await ensureWorktreeRegistry(forceSessionId);
|
|
1038
1117
|
ensureProjectRootWatcher();
|
|
@@ -1055,6 +1134,7 @@ export function closeBoardFileWatchers() {
|
|
|
1055
1134
|
worktreeReconcileFlight = null;
|
|
1056
1135
|
storeWatcher?.close();
|
|
1057
1136
|
storeWatcher = null;
|
|
1137
|
+
closeSessionDatabaseWatcher();
|
|
1058
1138
|
refsWatchers?.close();
|
|
1059
1139
|
refsWatchers = null;
|
|
1060
1140
|
registryWatcher?.close();
|
package/dist/guide.js
CHANGED
|
@@ -71,8 +71,9 @@ FRONTMATTER (YAML between the opening and closing --- lines; every field optiona
|
|
|
71
71
|
duplicates, globs/directories with a selector, and dead/ambiguous units all error loud. A
|
|
72
72
|
selector-scoped governor claims units, not the file, so it stays out of the \`owners\` bound
|
|
73
73
|
(spex spec owner still displays it, marked "(scoped)"). Anchors are optional.
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
Tree-sitter-backed \`.ts\`/\`.tsx\`/\`.py\`/\`.go\`/\`.rs\`/\`.java\`/\`.rb\` anchors recognize
|
|
75
|
+
structural function, method, class, interface, enum, and type declarations. Methods and nested
|
|
76
|
+
declarations use lexical qualified names such as
|
|
76
77
|
\`Class.method\`, \`outer.inner\`, or \`Outer.Inner.method\`; attached decorators belong to
|
|
77
78
|
the declaration's range. Runtime-created/assigned callables, imported aliases, and generated
|
|
78
79
|
names are outside this declaration extractor and therefore resolve as dead anchors.
|
|
@@ -306,7 +307,7 @@ PORTABILITY, and picking the right one is the whole discipline:
|
|
|
306
307
|
below); a targeted env override (SPEXCODE_CODEX_SERVER_CMD, …) still wins at its read site.
|
|
307
308
|
|
|
308
309
|
Rule of thumb — is the value TRUE FOR THE PROJECT or TRUE FOR THIS MACHINE? A branch name, a dashboard
|
|
309
|
-
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
|
|
310
311
|
PATH of a launcher wrapper or a TLS cert path are machine facts → gitignored spexcode.local.json.
|
|
311
312
|
Both files are optional; omit any field to take its default, except \`sessions.defaultLauncher\` when using
|
|
312
313
|
\`spex session new\` or the dashboard without an explicit launcher choice.
|
|
@@ -338,13 +339,8 @@ Example — a repo whose trunk is \`staging\`, not \`main\`:
|
|
|
338
339
|
dashboard.apiUrl the per-project backend the dashboard proxies to (read frontend-side). For a SHARED
|
|
339
340
|
install prefer the API_URL env var; apiUrl here is the default only when the dashboard
|
|
340
341
|
lives inside the project.
|
|
341
|
-
dashboard.showHeadlessLaunchers
|
|
342
|
-
include launchers whose harness declares itself headless in the dashboard New Session
|
|
343
|
-
picker. Default: false. This changes dashboard visibility only; explicit CLI
|
|
344
|
-
--launcher selection can still use every configured launcher.
|
|
345
342
|
Example:
|
|
346
|
-
{ "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch"
|
|
347
|
-
"showHeadlessLaunchers": false } }
|
|
343
|
+
{ "dashboard": { "title": "MyApp specs", "icon": "mdi:rocket-launch" } }
|
|
348
344
|
|
|
349
345
|
── HOST GATEWAY ($SPEXCODE_HOME/config.json — per-user host identity, never a project file) ──
|
|
350
346
|
gateway.icon the global /projects icon, using the same preset ids above. Default: "gateway".
|
|
@@ -402,14 +398,17 @@ A named launcher profile fixes BOTH a session's harness AND its exact launch com
|
|
|
402
398
|
by name with --launcher/the dashboard dropdown, and the chosen name is persisted on the record so a resume
|
|
403
399
|
reuses the same auth. There are NO magic built-ins: \`spex init\` SEEDS an ordinary named launcher for each
|
|
404
400
|
harness the adopter SELECTED (--harness), from the template pool
|
|
405
|
-
"claude" → { "harness": "claude", "cmd": "claude" }
|
|
406
401
|
"claude-headless" → { "harness": "claude-headless", "cmd": "claude" }
|
|
407
|
-
"
|
|
402
|
+
"claude" → { "harness": "claude", "cmd": "claude" }
|
|
408
403
|
"codex-headless" → { "harness": "codex-headless", "cmd": "codex --yolo" }
|
|
409
|
-
"
|
|
404
|
+
"codex" → { "harness": "codex", "cmd": "codex" }
|
|
410
405
|
"opencode-headless" → { "harness": "opencode-headless", "cmd": "opencode --auto" }
|
|
411
|
-
"
|
|
406
|
+
"opencode" → { "harness": "opencode", "cmd": "opencode" }
|
|
412
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.
|
|
413
412
|
The interactive profiles preserve each harness's normal permission model. \`opencode-headless\` and
|
|
414
413
|
\`codex-headless\` are deliberate seed exceptions: their terminal-free runs require \`opencode --auto\` and
|
|
415
414
|
\`codex --yolo\`; interactive profiles stay plain. Other automatic-permission commands are NEVER clean-init
|
|
@@ -546,8 +545,15 @@ behavior, decided per KIND (and, for a contract file, by its live CONTENT).
|
|
|
546
545
|
── THE FOUR KINDS (all fixed) ──
|
|
547
546
|
spec data .spec/ (incl .plugins/) + spexcode.json — ALWAYS tracked. Git is the database; there is
|
|
548
547
|
deliberately NO way to say "untrack the spec" in this schema.
|
|
549
|
-
|
|
550
|
-
|
|
548
|
+
(no delivery) \`spex init --harness none\` ("harnesses": []) adopts the spec tree, the lint and the git
|
|
549
|
+
hooks and writes NOTHING into any agent's config — the L0-only footprint.
|
|
550
|
+
machine facts spexcode.local.json, the hook shims, plugin bundles — NEVER tracked; always in the
|
|
551
|
+
per-clone exclude. A shim is a machine fact only while it is WHOLLY OURS: where the harness
|
|
552
|
+
discovers its hooks in a file that is ALSO your project config (.claude/settings.json,
|
|
553
|
+
.codex/hooks.json, .zcode/settings.json), SpexCode co-owns only its own hook entries —
|
|
554
|
+
your permissions/env/statusLine/hooks are merged around, never replaced, and uninstall
|
|
555
|
+
takes back exactly those entries. Such a file stays visible to git (hiding yours would be
|
|
556
|
+
data-loss shaped), so keep our absolute toolchain paths out of your commits.
|
|
551
557
|
artifacts the CLAUDE.md/AGENTS.md contract blocks + materialized skills/agents — derived, NEVER
|
|
552
558
|
tracked; hidden via .git/info/exclude. The host's tracked .gitignore is never touched.
|
|
553
559
|
run residue .worktrees/, the global store (~/.spexcode), .git/spexcode evidence — never tracked;
|
|
@@ -621,15 +627,20 @@ Use the session's file list when an artifact belongs in the human's hands:
|
|
|
621
627
|
spex session files retract <path> withdraw one path
|
|
622
628
|
|
|
623
629
|
Posting resolves a relative path from your current directory and records its absolute path beside the global
|
|
624
|
-
session record. It copies, moves, stages, and uploads NOTHING. The path is live: editing the file after
|
|
630
|
+
session record only after confirming it is a readable regular file. It copies, moves, stages, and uploads NOTHING. The path is live: editing the file after
|
|
625
631
|
posting changes what the human downloads. The reference is host-local; opening the session elsewhere cannot
|
|
626
632
|
make its path point at another machine's file.
|
|
627
633
|
|
|
634
|
+
Put raw run artifacts in a persistent directory OUTSIDE the product repository by default. A worktree artifact
|
|
635
|
+
makes merge readiness report a dirty tree and pressures generated evidence into the product commit. Before review,
|
|
636
|
+
run \`spex session files ls\`: a target that disappeared or became unreadable is printed as \`INVALID\` and must be
|
|
637
|
+
recreated or retracted; a valid path prints normally.
|
|
638
|
+
|
|
628
639
|
The session page's top-right files icon is grey while the list is empty. Once live, it opens the posted list;
|
|
629
640
|
choosing a path previews its current text or raster-image bytes in a pop-out, while the adjacent download tool
|
|
630
641
|
downloads it through the backend at that moment. Previews are limited to 2 MiB, text and PNG/JPEG/GIF/WebP;
|
|
631
|
-
other types and larger files say to download instead. A missing, moved, or unreadable target stays listed
|
|
632
|
-
reports that it no longer exists. The backend refuses a preview or download for any path not on that session's
|
|
642
|
+
other types and larger files say to download instead. A missing, moved, or unreadable target stays listed and is
|
|
643
|
+
marked invalid by the CLI; preview/download reports that it no longer exists. The backend refuses a preview or download for any path not on that session's
|
|
633
644
|
list.
|
|
634
645
|
|
|
635
646
|
This is the reverse of a dashboard attachment: [[file-attach]] sends human bytes to an agent. Files publishes
|
package/dist/harness-select.js
CHANGED
|
@@ -6,11 +6,17 @@ const KNOWN = HARNESSES.map((h) => h.id);
|
|
|
6
6
|
// the one-line repair every missing-selection error carries — the field is REQUIRED, never defaulted,
|
|
7
7
|
// because with many harnesses a silent "deliver everywhere" would litter the adopter's tree with artifacts
|
|
8
8
|
// for tools they never installed.
|
|
9
|
-
const MISSING = `spexcode.json has no "harnesses" field — the delivery targets are an EXPLICIT choice, never a default. Declare it, e.g. "harnesses": ["claude"] — members are native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}. Fresh adoption: \`spex init --harness <ids>\` stamps it.`;
|
|
9
|
+
const MISSING = `spexcode.json has no "harnesses" field — the delivery targets are an EXPLICIT choice, never a default. Declare it, e.g. "harnesses": ["claude"] — members are native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}, and [] means deliver into NO harness (the L0-only posture, \`spex init --harness none\`). Fresh adoption: \`spex init --harness <ids>\` stamps it.`;
|
|
10
|
+
// the CLI spelling of the empty set. `--harness none` is a whole-selection word, not a member: mixing it with
|
|
11
|
+
// a real target is a contradiction, so it only translates when it IS the entire flag (otherwise it falls
|
|
12
|
+
// through as an unknown id and resolveHarnessTargets says why).
|
|
13
|
+
const NO_DELIVERY = 'none';
|
|
10
14
|
// parse `spex init --harness <spec>` into the raw JSON members the `harnesses` field carries: a comma-
|
|
11
15
|
// separated list where `plugin:<folder>` means a plugin target and anything else is a native id. Pure
|
|
12
16
|
// spelling translation — legality (unknown ids, plugin exclusivity) stays with resolveHarnessTargets.
|
|
13
17
|
export function parseHarnessFlag(spec) {
|
|
18
|
+
if (spec.trim() === NO_DELIVERY)
|
|
19
|
+
return [];
|
|
14
20
|
return spec.split(',').map((s) => s.trim()).filter(Boolean)
|
|
15
21
|
.map((s) => (s.startsWith('plugin:') ? { plugin: s.slice('plugin:'.length) } : s));
|
|
16
22
|
}
|
|
@@ -22,15 +28,22 @@ export function resolveHarnessTargets(raw) {
|
|
|
22
28
|
throw new Error(MISSING);
|
|
23
29
|
if (!Array.isArray(raw))
|
|
24
30
|
throw new Error(`spexcode.json "harnesses" must be an ARRAY of targets (got ${typeof raw}). Members are native ids (${KNOWN.join(', ')}) or {"plugin":"<folder>"}.`);
|
|
31
|
+
// THE EMPTY SET IS A LEGAL, EXPLICIT CHOICE: deliver into no harness at all. That is the L0-only posture —
|
|
32
|
+
// the spec tree, the lint, and the git hooks, with zero artifacts written into any agent's config. It is
|
|
33
|
+
// distinct from a MISSING field (undefined/null, rejected above): [] is somebody saying "none", not
|
|
34
|
+
// somebody forgetting to choose. Everything downstream already handles it — materialize's erase phase
|
|
35
|
+
// prunes whatever a previous selection delivered, and the published allowlist admits nothing.
|
|
25
36
|
if (raw.length === 0)
|
|
26
|
-
|
|
37
|
+
return [];
|
|
27
38
|
const targets = [];
|
|
28
39
|
for (const m of raw) {
|
|
29
40
|
if (typeof m === 'string') {
|
|
30
41
|
if (m === 'plugin')
|
|
31
42
|
throw new Error(`spexcode.json "harnesses": a plugin target needs an EXPLICIT landing folder — write {"plugin":"<folder>"} (e.g. {"plugin":".adopter-a"}), not the bare string "plugin", because each host agent reads a different plugins dir.`);
|
|
43
|
+
if (m === NO_DELIVERY)
|
|
44
|
+
throw new Error(`spexcode.json "harnesses": "${NO_DELIVERY}" is not a member — it is the CLI spelling of the WHOLE selection being empty. Write "harnesses": [] for no delivery, or drop "${NO_DELIVERY}" and keep the real targets (\`spex init --harness none\` cannot be combined with another id).`);
|
|
32
45
|
if (!KNOWN.includes(m))
|
|
33
|
-
throw new Error(`spexcode.json "harnesses": unknown harness id "${m}" — known native ids are ${KNOWN.join(', ')}, or use {"plugin":"<folder>"}.`);
|
|
46
|
+
throw new Error(`spexcode.json "harnesses": unknown harness id "${m}" — known native ids are ${KNOWN.join(', ')}, or use {"plugin":"<folder>"}; [] (\`--harness ${NO_DELIVERY}\`) delivers into no harness at all.`);
|
|
34
47
|
targets.push({ kind: 'native', id: m });
|
|
35
48
|
}
|
|
36
49
|
else if (m && typeof m === 'object' && !Array.isArray(m) && 'plugin' in m) {
|
package/dist/harness.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SlashCommand } from './slash-commands.js';
|
|
2
|
-
import { type
|
|
2
|
+
import { type TranscriptReader } from '@spexcode/transcript';
|
|
3
3
|
import { type HarnessId } from '@spexcode/spec-core';
|
|
4
4
|
export type { HarnessId } from '@spexcode/spec-core';
|
|
5
5
|
export type HarnessLivenessRecord = {
|
|
@@ -23,6 +23,7 @@ export type TurnFailure = {
|
|
|
23
23
|
export type FailureSubscription = {
|
|
24
24
|
close(): void;
|
|
25
25
|
readonly closed: Promise<string | null>;
|
|
26
|
+
readonly ready?: Promise<boolean>;
|
|
26
27
|
};
|
|
27
28
|
export type DeliveryTransportState = {
|
|
28
29
|
kind: 'reachable';
|
|
@@ -58,7 +59,7 @@ export type SharedRuntimeDescriptor = {
|
|
|
58
59
|
mutationGuard?: (targetReferenceId: string, opts?: {
|
|
59
60
|
coldReceipt?: unknown;
|
|
60
61
|
}) => Promise<SharedRuntimeMutationGuard>;
|
|
61
|
-
probe(): Promise<SharedRuntimeProbe>;
|
|
62
|
+
probe(referenceIds?: readonly string[]): Promise<SharedRuntimeProbe>;
|
|
62
63
|
};
|
|
63
64
|
export type SharedRuntimeMutationGuard = {
|
|
64
65
|
healthy: boolean;
|
|
@@ -121,7 +122,7 @@ export interface Harness {
|
|
|
121
122
|
readonly events: readonly string[];
|
|
122
123
|
readonly ownsRendezvous: boolean;
|
|
123
124
|
readonly paneTitleIsSelfSummary: boolean;
|
|
124
|
-
|
|
125
|
+
readonly transcript: TranscriptReader;
|
|
125
126
|
launchCmd(id: string, runtimeDir?: string, cmd?: string): string;
|
|
126
127
|
baseCmd(cmd?: string): string;
|
|
127
128
|
oneShotTurn?(prompt: string, cmd?: string): {
|
|
@@ -139,8 +140,10 @@ export interface Harness {
|
|
|
139
140
|
agentDir(proj: string): string | null;
|
|
140
141
|
shim(dispatch: string, spex: string): {
|
|
141
142
|
content: string;
|
|
143
|
+
hooks?: Record<string, unknown[]>;
|
|
142
144
|
cmd: (e: string) => string;
|
|
143
145
|
};
|
|
146
|
+
shimOwnership: 'exclusive' | 'shared-json';
|
|
144
147
|
writeTrust(proj: string, cmdFor: (e: string) => string): readonly string[];
|
|
145
148
|
slashCommands(): SlashCommand[];
|
|
146
149
|
liveness(rec: HarnessLivenessRecord, tmuxAlive: boolean, runtimeDir?: string, pane?: PaneProbe, socketLive?: boolean): 'online' | 'offline';
|
|
@@ -226,9 +229,12 @@ export declare const rendezvousListening: (id: string, timeoutMs?: number) => Pr
|
|
|
226
229
|
export declare const codexAppServerSock: (dir?: string) => string;
|
|
227
230
|
export declare const codexAppServerPid: (dir?: string) => string;
|
|
228
231
|
export declare const codexAppServerReceipt: (dir?: string) => string;
|
|
229
|
-
export declare function deliverViaRendezvous(id: string, text: string, mid?: string): Promise<DispatchResult>;
|
|
232
|
+
export declare function deliverViaRendezvous(id: string, text: string, mid?: string, wallMs?: number): Promise<DispatchResult>;
|
|
230
233
|
export declare function deliverViaClaudeRendezvous(id: string, text: string, mid?: string, runtimeDir?: string): Promise<DispatchResult>;
|
|
231
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>;
|
|
232
238
|
type JsonRpc = {
|
|
233
239
|
id?: number;
|
|
234
240
|
method?: string;
|
|
@@ -242,6 +248,7 @@ type JsonRpc = {
|
|
|
242
248
|
export declare function codexHandshakeMessages(threadId: string): JsonRpc[];
|
|
243
249
|
export declare function codexInjectMessage(threadId: string, text: string, cwd: string | undefined, activeTurnId: string | null, id?: number, clientUserMessageId?: string): JsonRpc;
|
|
244
250
|
export declare function activeTurnIdFromThread(readResult: unknown): string | null;
|
|
251
|
+
export declare function codexObservedActiveTurnId(threadId: string): string | null;
|
|
245
252
|
export declare function codexBinary(codexCmd: string): string;
|
|
246
253
|
export type CodexThreadPolicy = {
|
|
247
254
|
approvalPolicy?: 'untrusted' | 'on-request' | 'never';
|
|
@@ -253,7 +260,14 @@ export declare function reportHeadlessTurnExit(id: string, harness: string, code
|
|
|
253
260
|
export declare function headlessTurnFailureShell(harness: string, swallow?: boolean): string;
|
|
254
261
|
export declare function sessionIdentityEnvVars(): string[];
|
|
255
262
|
export declare function codexLaunchCommand(id: string, codexCmd?: string, serverCmd?: string, dir?: string, attachTui?: boolean): string;
|
|
263
|
+
export declare const CODEX_TURN_OBSERVER_SUBSCRIBE_MS = 30000;
|
|
256
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
|
+
}>;
|
|
257
271
|
export declare const CODEX_THREAD_SOURCE_KINDS: readonly ["cli", "vscode", "exec", "appServer", "subAgent", "subAgentReview", "subAgentCompact", "subAgentThreadSpawn", "subAgentOther", "unknown"];
|
|
258
272
|
export declare function codexLoadedReferenceIds(sock: string): Promise<{
|
|
259
273
|
ok: true;
|
|
@@ -282,7 +296,7 @@ export declare function codexSharedRuntimeProbe(dir?: string, endpoint?: Readonl
|
|
|
282
296
|
receiptFile: string;
|
|
283
297
|
logFile: string;
|
|
284
298
|
socketPath: string;
|
|
285
|
-
}
|
|
299
|
+
}>, referenceIds?: readonly string[]): Promise<SharedRuntimeProbe>;
|
|
286
300
|
export declare function codexStartThreadParams(cwd?: string, bypassHookTrust?: boolean, shellEnv?: Record<string, string>, policy?: CodexThreadPolicy): Record<string, unknown>;
|
|
287
301
|
export declare function codexStartThread(sock: string, cwd?: string, bypassHookTrust?: boolean, shellEnv?: Record<string, string>, policy?: CodexThreadPolicy): Promise<{
|
|
288
302
|
ok: true;
|
|
@@ -301,6 +315,11 @@ export declare function codexRolloutBytes(threadId: string, root?: string): {
|
|
|
301
315
|
export declare function waitForCodexRollout(threadId: string, timeoutMs?: number): Promise<boolean>;
|
|
302
316
|
export declare function writeManagedBlock(file: string, body: string, comment?: readonly [string, string]): boolean;
|
|
303
317
|
export declare function removeManagedBlock(file: string, comment?: readonly [string, string], deleteIfEmpty?: boolean): void;
|
|
318
|
+
export declare function writeManagedJsonHooks(file: string, hooks: Record<string, unknown[]>): boolean;
|
|
319
|
+
export declare function removeManagedJsonHooks(file: string): void;
|
|
320
|
+
export declare function sharedShimHasHostContent(file: string): boolean;
|
|
321
|
+
export declare const GENERATED_MARK = "<!-- spexcode:generated -->";
|
|
322
|
+
export declare function isGeneratedArtifact(file: string): boolean;
|
|
304
323
|
export declare function codexHookHash(snakeEvent: string, command: string, timeout?: number, asyncFlag?: boolean): string;
|
|
305
324
|
export declare function writeCodexTrust(proj: string, events: readonly string[], cmdFor: (e: string) => string): string;
|
|
306
325
|
export declare function paneTreeRunsCodex(pane?: PaneProbe): boolean;
|
|
@@ -331,7 +350,6 @@ export type LauncherDefault = {
|
|
|
331
350
|
error: string | null;
|
|
332
351
|
};
|
|
333
352
|
export declare function launcherList(root?: string): Launcher[];
|
|
334
|
-
export declare function dashboardLauncherList(root?: string): Launcher[];
|
|
335
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\"}})";
|
|
336
354
|
export declare function defaultLauncher(root?: string): string;
|
|
337
355
|
export declare function launcherDefault(root?: string): LauncherDefault;
|