@karmaniverous/jeeves-meta 0.16.1 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,10 +14,12 @@ HTTP service for the Jeeves knowledge synthesis engine. Provides a Fastify API,
14
14
  - **Cross-meta references** — `_crossRefs` declares relationships to other metas; referenced `_content` included as architect/builder context
15
15
  - **Archive management** — timestamped snapshots with configurable pruning
16
16
  - **Lock staging** — write to `.lock` → copy to `meta.json` → archive (crash-safe)
17
+ - **Staging file retry** — configurable retries (`stagingRetries`, `stagingRetryDelayMs`) when sub-agent output file is not yet visible after session completion (network/NFS latency)
18
+ - **Preview delta cap** — `/preview` response includes `deltaFilesTruncated` flag and `deltaCount` total when delta files exceed `previewDeltaFilesCap`
17
19
  - **Virtual rule registration** — registers 3 watcher inference rules at startup with retry
18
20
  - **Progress reporting** — real-time synthesis events via gateway channel messages
19
21
  - **Graceful shutdown** — stop scheduler, release locks, close server
20
- - **Built-in prompts** — default architect and critic prompts ship with the package; optional config overrides via `@file:` or inline strings
22
+ - **Built-in prompts** — default architect and critic prompts ship with the package
21
23
  - **Handlebars templates** — prompts compiled with `{ config, meta, scope }` context; architect can write template expressions into builder briefs
22
24
  - **Config hot-reload** — all synthesis parameters reload without restart; restart-required fields (port, URLs) warn on change
23
25
  - **Auto-seed policy** — config-driven declarative `.meta/` creation via `autoSeed` rules
@@ -714,8 +714,6 @@ const RESTART_REQUIRED_FIELDS = [
714
714
  'watcherUrl',
715
715
  'gatewayUrl',
716
716
  'gatewayApiKey',
717
- 'defaultArchitect',
718
- 'defaultCritic',
719
717
  ];
720
718
  let runtime = null;
721
719
  /** Register the active service runtime for config-apply hot reload. */
@@ -795,6 +793,12 @@ const serviceConfigSchema = metaConfigSchema.extend({
795
793
  reportTarget: z.string().optional(),
796
794
  /** Optional base URL for the service, used to construct entity links in progress reports. */
797
795
  serverBaseUrl: z.string().optional(),
796
+ /**
797
+ * Optional mapping of server drive labels to absolute filesystem paths.
798
+ * Used on Linux to resolve absolute paths to jeeves-server browse paths.
799
+ * Example: `{ "content": "/opt/jeeves/content" }`
800
+ */
801
+ serverDriveRoots: z.record(z.string(), z.string()).optional(),
798
802
  /** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
799
803
  watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
800
804
  /** Logging configuration. */
@@ -806,12 +810,20 @@ const serviceConfigSchema = metaConfigSchema.extend({
806
810
  * Rules are evaluated in order; last match wins for steer/crossRefs.
807
811
  */
808
812
  autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
813
+ /** Optional workspace directory for synthesis staging files. Defaults to `os.tmpdir() + '/jeeves-meta'`. */
814
+ workspaceDir: z.string().optional(),
815
+ /** Max retries when staging file not yet visible after session completion. Default: 10. */
816
+ stagingRetries: z.number().int().min(0).default(10),
817
+ /** Delay between staging file retries in ms. Default: 250. */
818
+ stagingRetryDelayMs: z.number().int().min(0).default(250),
819
+ /** Maximum number of delta files returned in /preview response. Default: 50. */
820
+ previewDeltaFilesCap: z.number().int().min(1).default(50),
809
821
  });
810
822
 
811
823
  /**
812
824
  * Load and resolve jeeves-meta service config.
813
825
  *
814
- * Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
826
+ * Supports environment-variable substitution (dollar-brace pattern).
815
827
  *
816
828
  * @module configLoader
817
829
  */
@@ -843,24 +855,10 @@ function substituteEnvVars(value) {
843
855
  }
844
856
  return value;
845
857
  }
846
- /**
847
- * Resolve \@file: references in a config value.
848
- *
849
- * @param value - String value that may start with "\@file:".
850
- * @param baseDir - Base directory for resolving relative paths.
851
- * @returns The resolved string (file contents or original value).
852
- */
853
- function resolveFileRef(value, baseDir) {
854
- if (!value.startsWith('@file:'))
855
- return value;
856
- const filePath = join(baseDir, value.slice(6));
857
- return readFileSync(filePath, 'utf8');
858
- }
859
858
  /**
860
859
  * Load service config from a JSON file.
861
860
  *
862
- * Resolves \@file: references for defaultArchitect and defaultCritic,
863
- * and substitutes environment-variable placeholders throughout.
861
+ * Substitutes environment-variable placeholders throughout.
864
862
  *
865
863
  * @param configPath - Path to config JSON file.
866
864
  * @returns Validated ServiceConfig.
@@ -868,13 +866,6 @@ function resolveFileRef(value, baseDir) {
868
866
  function loadServiceConfig(configPath) {
869
867
  const rawText = readFileSync(configPath, 'utf8');
870
868
  const raw = substituteEnvVars(JSON.parse(rawText));
871
- const baseDir = dirname(configPath);
872
- if (typeof raw['defaultArchitect'] === 'string') {
873
- raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
874
- }
875
- if (typeof raw['defaultCritic'] === 'string') {
876
- raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
877
- }
878
869
  return serviceConfigSchema.parse(raw);
879
870
  }
880
871
 
@@ -933,12 +924,16 @@ class GatewayExecutor {
933
924
  apiKey;
934
925
  pollIntervalMs;
935
926
  workspaceDir;
927
+ stagingRetries;
928
+ stagingRetryDelayMs;
936
929
  controller = new AbortController();
937
930
  constructor(options = {}) {
938
931
  this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:18789').replace(/\/+$/, '');
939
932
  this.apiKey = options.apiKey;
940
933
  this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
941
934
  this.workspaceDir = options.workspaceDir ?? join(tmpdir(), 'jeeves-meta');
935
+ this.stagingRetries = options.stagingRetries ?? 10;
936
+ this.stagingRetryDelayMs = options.stagingRetryDelayMs ?? 250;
942
937
  }
943
938
  /** Remove a temp output file if it exists. */
944
939
  cleanupOutputFile(outputPath) {
@@ -1168,10 +1163,23 @@ class GatewayExecutor {
1168
1163
  // No output or partial output — throw for _state recovery (§3.16.6)
1169
1164
  throw new SpawnTimeoutError('Gateway-side timeout detected (session status: timeout)', outputPath);
1170
1165
  }
1171
- // Normal completion — read output from file
1166
+ // Normal completion — read staging file with retry for delayed visibility
1172
1167
  const output = this.readStagingFile(outputPath);
1173
1168
  if (output !== undefined)
1174
1169
  return { output, tokens };
1170
+ // Staging file not yet visible — retry with bounded grace window
1171
+ for (let i = 0; i < this.stagingRetries; i++) {
1172
+ await sleepAsync(this.stagingRetryDelayMs);
1173
+ // Check abort after yield point — signal may have been
1174
+ // set externally (e.g. operator abort) during the sleep.
1175
+ if (this.aborted) {
1176
+ this.cleanupOutputFile(outputPath);
1177
+ throw new SpawnAbortedError();
1178
+ }
1179
+ const retryOutput = this.readStagingFile(outputPath);
1180
+ if (retryOutput !== undefined)
1181
+ return { output: retryOutput, tokens };
1182
+ }
1175
1183
  // Fallback: extract from message content if file wasn't written.
1176
1184
  // Skip ANNOUNCE_SKIP sentinel messages — the real output is in
1177
1185
  // a preceding assistant message (the file write).
@@ -1321,9 +1329,6 @@ function packageDirectorySync({cwd, ignoreTypeOnlyPackageJson} = {}) {
1321
1329
  * Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
1322
1330
  * Loaded at runtime relative to the compiled module location.
1323
1331
  *
1324
- * Users can override via `defaultArchitect` / `defaultCritic` in the service
1325
- * config. Most installations should use the built-in defaults.
1326
- *
1327
1332
  * @module prompts
1328
1333
  */
1329
1334
  const packageRoot = packageDirectorySync({
@@ -1811,7 +1816,7 @@ function buildArchitectTask(ctx, meta, config) {
1811
1816
  const sections = [
1812
1817
  `# jeeves-meta · ARCHITECT · ${ctx.path}`,
1813
1818
  '',
1814
- config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
1819
+ DEFAULT_ARCHITECT_PROMPT,
1815
1820
  '',
1816
1821
  '## SCOPE',
1817
1822
  `Path: ${ctx.path}`,
@@ -1860,7 +1865,7 @@ function buildBuilderTask(ctx, meta, config) {
1860
1865
  includeSteer: false,
1861
1866
  feedbackHeading: '## FEEDBACK FROM CRITIC',
1862
1867
  });
1863
- sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.');
1868
+ sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.', '', 'IMPORTANT: Do not call sessions_spawn or sessions_yield. Read all files directly using the tools', 'available in your session. Do not attempt to parallelize work by spawning sub-agents.');
1864
1869
  return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
1865
1870
  }
1866
1871
  /**
@@ -1875,7 +1880,7 @@ function buildCriticTask(ctx, meta, config) {
1875
1880
  const sections = [
1876
1881
  `# jeeves-meta · CRITIC · ${ctx.path}`,
1877
1882
  '',
1878
- config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
1883
+ DEFAULT_CRITIC_PROMPT,
1879
1884
  '',
1880
1885
  '## SYNTHESIS TO EVALUATE',
1881
1886
  meta._content ?? '(No content produced)',
@@ -2292,8 +2297,8 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
2292
2297
  // through real builder cycles, architect runs with the current prompt and
2293
2298
  // the snapshot updates. Coupling prompt changes to invalidation causes a
2294
2299
  // corpus-wide synthesis storm (see #163).
2295
- const architectChanged = isPromptStale(meta._architect, config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT);
2296
- const criticChanged = isPromptStale(meta._critic, config.defaultCritic ?? DEFAULT_CRITIC_PROMPT);
2300
+ const architectChanged = isPromptStale(meta._architect, DEFAULT_ARCHITECT_PROMPT);
2301
+ const criticChanged = isPromptStale(meta._critic, DEFAULT_CRITIC_PROMPT);
2297
2302
  const effectiveSynthesisCount = meta._synthesisCount ?? 0;
2298
2303
  // _crossRefs declaration change
2299
2304
  const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
@@ -2540,10 +2545,11 @@ function parseArchitectOutput(output) {
2540
2545
  /**
2541
2546
  * Parse builder output. The builder returns JSON with _content and optional fields.
2542
2547
  *
2543
- * Attempts JSON parse first. If that fails, treats the entire output as _content.
2548
+ * Attempts JSON extraction via multiple strategies. Returns null if all strategies fail
2549
+ * (e.g. the output is plain text, self-talk, or a sub-agent delegation response).
2544
2550
  *
2545
2551
  * @param output - Raw subprocess output.
2546
- * @returns Parsed builder output with content and structured fields.
2552
+ * @returns Parsed builder output, or null if output is not valid builder JSON.
2547
2553
  */
2548
2554
  function parseBuilderOutput(output) {
2549
2555
  const trimmed = stripSentinel(output);
@@ -2572,8 +2578,8 @@ function parseBuilderOutput(output) {
2572
2578
  if (result)
2573
2579
  return result;
2574
2580
  }
2575
- // Fallback: treat entire output as content
2576
- return { content: trimmed, fields: {} };
2581
+ // All JSON strategies failed not valid builder output
2582
+ return null;
2577
2583
  }
2578
2584
  /** Try to parse a string as JSON and extract builder output fields. */
2579
2585
  function tryParseJson(str) {
@@ -2701,7 +2707,7 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
2701
2707
  ps = architectSuccess(ps);
2702
2708
  const architectUpdates = {
2703
2709
  _builder: builderBrief,
2704
- _architect: config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
2710
+ _architect: DEFAULT_ARCHITECT_PROMPT,
2705
2711
  _synthesisCount: 0,
2706
2712
  _architectTokens: architectTokens,
2707
2713
  _generatedAt: new Date().toISOString(),
@@ -2773,6 +2779,12 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
2773
2779
  return { executed: true, phaseState: ps, updatedMeta };
2774
2780
  }
2775
2781
  const builderOutput = parseBuilderOutput(rawOutput);
2782
+ if (!builderOutput) {
2783
+ throw new Error('Builder output is not valid JSON — expected { _content|content: string, ... }. ' +
2784
+ 'Raw output (' +
2785
+ rawOutput.length.toString() +
2786
+ ' chars) did not match any JSON extraction strategy.');
2787
+ }
2776
2788
  const builderTokens = result.tokens;
2777
2789
  // Builder success: builder → fresh, critic → pending
2778
2790
  ps = builderSuccess(ps);
@@ -2804,7 +2816,8 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
2804
2816
  try {
2805
2817
  const raw = await readFile(err.outputPath, 'utf8');
2806
2818
  const partial = parseBuilderOutput(raw);
2807
- if (partial.state !== undefined &&
2819
+ if (partial !== null &&
2820
+ partial.state !== undefined &&
2808
2821
  JSON.stringify(partial.state) !== JSON.stringify(currentMeta._state)) {
2809
2822
  partialState = { _state: partial.state };
2810
2823
  }
@@ -2847,7 +2860,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
2847
2860
  const cycleComplete = isFullyFresh(ps);
2848
2861
  const updates = {
2849
2862
  _feedback: feedback,
2850
- _critic: config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
2863
+ _critic: DEFAULT_CRITIC_PROMPT,
2851
2864
  _criticTokens: criticTokens,
2852
2865
  _error: undefined,
2853
2866
  };
@@ -3069,29 +3082,62 @@ function encodePathSegments(p) {
3069
3082
  .map((seg) => encodeURIComponent(seg))
3070
3083
  .join('/');
3071
3084
  }
3085
+ /**
3086
+ * Resolve a filesystem path to a jeeves-server browse path segment.
3087
+ *
3088
+ * - Windows: `j:/domains/foo` → `/j/domains/foo` (drive-letter transform).
3089
+ * - Linux with serverDriveRoots: `/opt/jeeves/content/slack` + roots
3090
+ * `{ content: "/opt/jeeves/content" }` → `/content/slack`.
3091
+ * - Linux without serverDriveRoots: returns the normalized path unchanged
3092
+ * (browse link will be invalid, but this is the pre-existing behavior).
3093
+ */
3094
+ function resolveServerPath(path, serverDriveRoots) {
3095
+ const normalized = normalizePath(path);
3096
+ // Windows: drive letter present — use existing transform
3097
+ if (/^[A-Za-z]:/.test(normalized)) {
3098
+ return normalized.replace(/^([A-Za-z]):/, '/$1');
3099
+ }
3100
+ // Linux: attempt serverDriveRoots resolution (longest/most-specific root wins)
3101
+ if (serverDriveRoots) {
3102
+ const sorted = Object.entries(serverDriveRoots)
3103
+ .map(([label, root]) => [label, normalizePath(root).replace(/\/+$/, '')])
3104
+ .sort((a, b) => b[1].length - a[1].length);
3105
+ for (const [label, normalizedRoot] of sorted) {
3106
+ if (normalized === normalizedRoot ||
3107
+ normalized.startsWith(normalizedRoot + '/')) {
3108
+ const relative = normalized
3109
+ .slice(normalizedRoot.length)
3110
+ .replace(/^\//, '');
3111
+ return `/${label}${relative ? '/' + relative : ''}`;
3112
+ }
3113
+ }
3114
+ }
3115
+ // Fallback: no drive roots configured or no match — return as-is
3116
+ return normalized;
3117
+ }
3072
3118
  /** Build a link (or plain path) to the owner directory. */
3073
- function buildDirectoryLink(path, serverBaseUrl) {
3074
- const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
3075
- const encoded = encodePathSegments(normalized);
3119
+ function buildDirectoryLink(path, serverBaseUrl, serverDriveRoots) {
3120
+ const resolved = resolveServerPath(path, serverDriveRoots);
3121
+ const encoded = encodePathSegments(resolved);
3076
3122
  if (!serverBaseUrl)
3077
- return normalized;
3123
+ return resolved;
3078
3124
  const base = serverBaseUrl.replace(/\/+$/, '');
3079
3125
  return `${base}/path${encoded}`;
3080
3126
  }
3081
3127
  /** Build a link (or plain path) to the entity's meta.json output file. */
3082
- function buildMetaJsonLink(path, serverBaseUrl) {
3083
- const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
3084
- const metaJsonPath = `${normalized}/.meta/meta.json`;
3128
+ function buildMetaJsonLink(path, serverBaseUrl, serverDriveRoots) {
3129
+ const resolved = resolveServerPath(path, serverDriveRoots);
3130
+ const metaJsonPath = `${resolved}/.meta/meta.json`;
3085
3131
  const encoded = encodePathSegments(metaJsonPath);
3086
3132
  if (!serverBaseUrl)
3087
3133
  return metaJsonPath;
3088
3134
  const base = serverBaseUrl.replace(/\/+$/, '');
3089
3135
  return `${base}/path${encoded}`;
3090
3136
  }
3091
- function formatProgressEvent(event, serverBaseUrl) {
3137
+ function formatProgressEvent(event, serverBaseUrl, serverDriveRoots) {
3092
3138
  switch (event.type) {
3093
3139
  case 'synthesis_start': {
3094
- const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
3140
+ const dirLink = buildDirectoryLink(event.path, serverBaseUrl, serverDriveRoots);
3095
3141
  return `🔬 Started meta synthesis: ${dirLink}`;
3096
3142
  }
3097
3143
  case 'phase_start': {
@@ -3107,7 +3153,7 @@ function formatProgressEvent(event, serverBaseUrl) {
3107
3153
  return ` ✅ ${phase} complete (${tokenStr} / ${duration})`;
3108
3154
  }
3109
3155
  case 'synthesis_complete': {
3110
- const metaLink = buildMetaJsonLink(event.path, serverBaseUrl);
3156
+ const metaLink = buildMetaJsonLink(event.path, serverBaseUrl, serverDriveRoots);
3111
3157
  const tokenStr = formatTokens(event.tokens);
3112
3158
  const duration = event.durationMs !== undefined
3113
3159
  ? formatSeconds(event.durationMs)
@@ -3115,7 +3161,7 @@ function formatProgressEvent(event, serverBaseUrl) {
3115
3161
  return `✅ Completed: ${metaLink} (${tokenStr} / ${duration})`;
3116
3162
  }
3117
3163
  case 'error': {
3118
- const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
3164
+ const dirLink = buildDirectoryLink(event.path, serverBaseUrl, serverDriveRoots);
3119
3165
  const phase = event.phase ? `${titleCasePhase(event.phase)} ` : '';
3120
3166
  const error = event.error ?? 'Unknown error';
3121
3167
  return `❌ Synthesis failed at ${phase}phase: ${dirLink}\n Error: ${error}`;
@@ -3138,7 +3184,7 @@ class ProgressReporter {
3138
3184
  const target = this.config.reportTarget ?? this.config.reportChannel;
3139
3185
  if (!target)
3140
3186
  return;
3141
- const message = formatProgressEvent(event, this.config.serverBaseUrl);
3187
+ const message = formatProgressEvent(event, this.config.serverBaseUrl, this.config.serverDriveRoots);
3142
3188
  const url = new URL('/tools/invoke', this.config.gatewayUrl);
3143
3189
  const args = {
3144
3190
  action: 'send',
@@ -4452,9 +4498,10 @@ function registerPreviewRoute(app, deps) {
4452
4498
  ownedFiles: scopeFiles.length,
4453
4499
  childMetas: targetNode.children.length,
4454
4500
  deltaFiles: deltaFiles
4455
- .slice(0, 50)
4501
+ .slice(0, config.previewDeltaFilesCap)
4456
4502
  .map((f) => ({ path: f, action: 'modified' })),
4457
4503
  deltaCount: deltaFiles.length,
4504
+ deltaFilesTruncated: deltaFiles.length > config.previewDeltaFilesCap,
4458
4505
  },
4459
4506
  estimatedTokens,
4460
4507
  // New phase-state fields (additive)
@@ -5156,6 +5203,9 @@ async function startService(config, configPath) {
5156
5203
  const executor = new GatewayExecutor({
5157
5204
  gatewayUrl: config.gatewayUrl,
5158
5205
  apiKey: config.gatewayApiKey,
5206
+ workspaceDir: config.workspaceDir,
5207
+ stagingRetries: config.stagingRetries,
5208
+ stagingRetryDelayMs: config.stagingRetryDelayMs,
5159
5209
  });
5160
5210
  // Runtime stats (mutable, shared with routes)
5161
5211
  const stats = {
@@ -15,7 +15,7 @@ import type { ServiceConfig } from './schema/config.js';
15
15
  * Shared between the descriptor's `onConfigApply` and the file-watcher
16
16
  * hot-reload in `bootstrap.ts`.
17
17
  */
18
- export declare const RESTART_REQUIRED_FIELDS: readonly ["port", "watcherUrl", "gatewayUrl", "gatewayApiKey", "defaultArchitect", "defaultCritic"];
18
+ export declare const RESTART_REQUIRED_FIELDS: readonly ["port", "watcherUrl", "gatewayUrl", "gatewayApiKey"];
19
19
  interface ConfigHotReloadRuntime {
20
20
  config: ServiceConfig;
21
21
  logger: Logger;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Load and resolve jeeves-meta service config.
3
3
  *
4
- * Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
4
+ * Supports environment-variable substitution (dollar-brace pattern).
5
5
  *
6
6
  * @module configLoader
7
7
  */
@@ -28,8 +28,7 @@ export declare function resolveConfigPath(args: string[]): string;
28
28
  /**
29
29
  * Load service config from a JSON file.
30
30
  *
31
- * Resolves \@file: references for defaultArchitect and defaultCritic,
32
- * and substitutes environment-variable placeholders throughout.
31
+ * Substitutes environment-variable placeholders throughout.
33
32
  *
34
33
  * @param configPath - Path to config JSON file.
35
34
  * @returns Validated ServiceConfig.
@@ -18,6 +18,10 @@ export interface GatewayExecutorOptions {
18
18
  pollIntervalMs?: number;
19
19
  /** Workspace directory for output staging. Default: OS temp dir + /jeeves-meta. */
20
20
  workspaceDir?: string;
21
+ /** Max retries when staging file not yet visible after session completion. Default: 10. */
22
+ stagingRetries?: number;
23
+ /** Delay between retries in ms. Default: 250. */
24
+ stagingRetryDelayMs?: number;
21
25
  }
22
26
  /**
23
27
  * MetaExecutor that spawns OpenClaw sessions via the gateway's
@@ -32,6 +36,8 @@ export declare class GatewayExecutor implements MetaExecutor {
32
36
  private readonly apiKey;
33
37
  private readonly pollIntervalMs;
34
38
  private readonly workspaceDir;
39
+ private readonly stagingRetries;
40
+ private readonly stagingRetryDelayMs;
35
41
  private controller;
36
42
  constructor(options?: GatewayExecutorOptions);
37
43
  /** Remove a temp output file if it exists. */
package/dist/index.js CHANGED
@@ -1203,8 +1203,6 @@ const RESTART_REQUIRED_FIELDS = [
1203
1203
  'watcherUrl',
1204
1204
  'gatewayUrl',
1205
1205
  'gatewayApiKey',
1206
- 'defaultArchitect',
1207
- 'defaultCritic',
1208
1206
  ];
1209
1207
  let runtime = null;
1210
1208
  /** Register the active service runtime for config-apply hot reload. */
@@ -1284,6 +1282,12 @@ const serviceConfigSchema = metaConfigSchema.extend({
1284
1282
  reportTarget: z.string().optional(),
1285
1283
  /** Optional base URL for the service, used to construct entity links in progress reports. */
1286
1284
  serverBaseUrl: z.string().optional(),
1285
+ /**
1286
+ * Optional mapping of server drive labels to absolute filesystem paths.
1287
+ * Used on Linux to resolve absolute paths to jeeves-server browse paths.
1288
+ * Example: `{ "content": "/opt/jeeves/content" }`
1289
+ */
1290
+ serverDriveRoots: z.record(z.string(), z.string()).optional(),
1287
1291
  /** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
1288
1292
  watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
1289
1293
  /** Logging configuration. */
@@ -1295,12 +1299,20 @@ const serviceConfigSchema = metaConfigSchema.extend({
1295
1299
  * Rules are evaluated in order; last match wins for steer/crossRefs.
1296
1300
  */
1297
1301
  autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
1302
+ /** Optional workspace directory for synthesis staging files. Defaults to `os.tmpdir() + '/jeeves-meta'`. */
1303
+ workspaceDir: z.string().optional(),
1304
+ /** Max retries when staging file not yet visible after session completion. Default: 10. */
1305
+ stagingRetries: z.number().int().min(0).default(10),
1306
+ /** Delay between staging file retries in ms. Default: 250. */
1307
+ stagingRetryDelayMs: z.number().int().min(0).default(250),
1308
+ /** Maximum number of delta files returned in /preview response. Default: 50. */
1309
+ previewDeltaFilesCap: z.number().int().min(1).default(50),
1298
1310
  });
1299
1311
 
1300
1312
  /**
1301
1313
  * Load and resolve jeeves-meta service config.
1302
1314
  *
1303
- * Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
1315
+ * Supports environment-variable substitution (dollar-brace pattern).
1304
1316
  *
1305
1317
  * @module configLoader
1306
1318
  */
@@ -1332,19 +1344,6 @@ function substituteEnvVars(value) {
1332
1344
  }
1333
1345
  return value;
1334
1346
  }
1335
- /**
1336
- * Resolve \@file: references in a config value.
1337
- *
1338
- * @param value - String value that may start with "\@file:".
1339
- * @param baseDir - Base directory for resolving relative paths.
1340
- * @returns The resolved string (file contents or original value).
1341
- */
1342
- function resolveFileRef(value, baseDir) {
1343
- if (!value.startsWith('@file:'))
1344
- return value;
1345
- const filePath = join(baseDir, value.slice(6));
1346
- return readFileSync(filePath, 'utf8');
1347
- }
1348
1347
  /**
1349
1348
  * Migrate legacy config path to the new canonical location.
1350
1349
  *
@@ -1393,8 +1392,7 @@ function resolveConfigPath(args) {
1393
1392
  /**
1394
1393
  * Load service config from a JSON file.
1395
1394
  *
1396
- * Resolves \@file: references for defaultArchitect and defaultCritic,
1397
- * and substitutes environment-variable placeholders throughout.
1395
+ * Substitutes environment-variable placeholders throughout.
1398
1396
  *
1399
1397
  * @param configPath - Path to config JSON file.
1400
1398
  * @returns Validated ServiceConfig.
@@ -1402,13 +1400,6 @@ function resolveConfigPath(args) {
1402
1400
  function loadServiceConfig(configPath) {
1403
1401
  const rawText = readFileSync(configPath, 'utf8');
1404
1402
  const raw = substituteEnvVars(JSON.parse(rawText));
1405
- const baseDir = dirname(configPath);
1406
- if (typeof raw['defaultArchitect'] === 'string') {
1407
- raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
1408
- }
1409
- if (typeof raw['defaultCritic'] === 'string') {
1410
- raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
1411
- }
1412
1403
  return serviceConfigSchema.parse(raw);
1413
1404
  }
1414
1405
 
@@ -1467,12 +1458,16 @@ class GatewayExecutor {
1467
1458
  apiKey;
1468
1459
  pollIntervalMs;
1469
1460
  workspaceDir;
1461
+ stagingRetries;
1462
+ stagingRetryDelayMs;
1470
1463
  controller = new AbortController();
1471
1464
  constructor(options = {}) {
1472
1465
  this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:18789').replace(/\/+$/, '');
1473
1466
  this.apiKey = options.apiKey;
1474
1467
  this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1475
1468
  this.workspaceDir = options.workspaceDir ?? join(tmpdir(), 'jeeves-meta');
1469
+ this.stagingRetries = options.stagingRetries ?? 10;
1470
+ this.stagingRetryDelayMs = options.stagingRetryDelayMs ?? 250;
1476
1471
  }
1477
1472
  /** Remove a temp output file if it exists. */
1478
1473
  cleanupOutputFile(outputPath) {
@@ -1702,10 +1697,23 @@ class GatewayExecutor {
1702
1697
  // No output or partial output — throw for _state recovery (§3.16.6)
1703
1698
  throw new SpawnTimeoutError('Gateway-side timeout detected (session status: timeout)', outputPath);
1704
1699
  }
1705
- // Normal completion — read output from file
1700
+ // Normal completion — read staging file with retry for delayed visibility
1706
1701
  const output = this.readStagingFile(outputPath);
1707
1702
  if (output !== undefined)
1708
1703
  return { output, tokens };
1704
+ // Staging file not yet visible — retry with bounded grace window
1705
+ for (let i = 0; i < this.stagingRetries; i++) {
1706
+ await sleepAsync(this.stagingRetryDelayMs);
1707
+ // Check abort after yield point — signal may have been
1708
+ // set externally (e.g. operator abort) during the sleep.
1709
+ if (this.aborted) {
1710
+ this.cleanupOutputFile(outputPath);
1711
+ throw new SpawnAbortedError();
1712
+ }
1713
+ const retryOutput = this.readStagingFile(outputPath);
1714
+ if (retryOutput !== undefined)
1715
+ return { output: retryOutput, tokens };
1716
+ }
1709
1717
  // Fallback: extract from message content if file wasn't written.
1710
1718
  // Skip ANNOUNCE_SKIP sentinel messages — the real output is in
1711
1719
  // a preceding assistant message (the file write).
@@ -1767,9 +1775,6 @@ function createLogger(config) {
1767
1775
  * Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
1768
1776
  * Loaded at runtime relative to the compiled module location.
1769
1777
  *
1770
- * Users can override via `defaultArchitect` / `defaultCritic` in the service
1771
- * config. Most installations should use the built-in defaults.
1772
- *
1773
1778
  * @module prompts
1774
1779
  */
1775
1780
  const packageRoot = packageDirectorySync({
@@ -2069,7 +2074,7 @@ function buildArchitectTask(ctx, meta, config) {
2069
2074
  const sections = [
2070
2075
  `# jeeves-meta · ARCHITECT · ${ctx.path}`,
2071
2076
  '',
2072
- config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
2077
+ DEFAULT_ARCHITECT_PROMPT,
2073
2078
  '',
2074
2079
  '## SCOPE',
2075
2080
  `Path: ${ctx.path}`,
@@ -2118,7 +2123,7 @@ function buildBuilderTask(ctx, meta, config) {
2118
2123
  includeSteer: false,
2119
2124
  feedbackHeading: '## FEEDBACK FROM CRITIC',
2120
2125
  });
2121
- sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.');
2126
+ sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.', '', 'IMPORTANT: Do not call sessions_spawn or sessions_yield. Read all files directly using the tools', 'available in your session. Do not attempt to parallelize work by spawning sub-agents.');
2122
2127
  return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
2123
2128
  }
2124
2129
  /**
@@ -2133,7 +2138,7 @@ function buildCriticTask(ctx, meta, config) {
2133
2138
  const sections = [
2134
2139
  `# jeeves-meta · CRITIC · ${ctx.path}`,
2135
2140
  '',
2136
- config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
2141
+ DEFAULT_CRITIC_PROMPT,
2137
2142
  '',
2138
2143
  '## SYNTHESIS TO EVALUATE',
2139
2144
  meta._content ?? '(No content produced)',
@@ -2550,8 +2555,8 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
2550
2555
  // through real builder cycles, architect runs with the current prompt and
2551
2556
  // the snapshot updates. Coupling prompt changes to invalidation causes a
2552
2557
  // corpus-wide synthesis storm (see #163).
2553
- const architectChanged = isPromptStale(meta._architect, config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT);
2554
- const criticChanged = isPromptStale(meta._critic, config.defaultCritic ?? DEFAULT_CRITIC_PROMPT);
2558
+ const architectChanged = isPromptStale(meta._architect, DEFAULT_ARCHITECT_PROMPT);
2559
+ const criticChanged = isPromptStale(meta._critic, DEFAULT_CRITIC_PROMPT);
2555
2560
  const effectiveSynthesisCount = meta._synthesisCount ?? 0;
2556
2561
  // _crossRefs declaration change
2557
2562
  const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
@@ -2798,10 +2803,11 @@ function parseArchitectOutput(output) {
2798
2803
  /**
2799
2804
  * Parse builder output. The builder returns JSON with _content and optional fields.
2800
2805
  *
2801
- * Attempts JSON parse first. If that fails, treats the entire output as _content.
2806
+ * Attempts JSON extraction via multiple strategies. Returns null if all strategies fail
2807
+ * (e.g. the output is plain text, self-talk, or a sub-agent delegation response).
2802
2808
  *
2803
2809
  * @param output - Raw subprocess output.
2804
- * @returns Parsed builder output with content and structured fields.
2810
+ * @returns Parsed builder output, or null if output is not valid builder JSON.
2805
2811
  */
2806
2812
  function parseBuilderOutput(output) {
2807
2813
  const trimmed = stripSentinel(output);
@@ -2830,8 +2836,8 @@ function parseBuilderOutput(output) {
2830
2836
  if (result)
2831
2837
  return result;
2832
2838
  }
2833
- // Fallback: treat entire output as content
2834
- return { content: trimmed, fields: {} };
2839
+ // All JSON strategies failed not valid builder output
2840
+ return null;
2835
2841
  }
2836
2842
  /** Try to parse a string as JSON and extract builder output fields. */
2837
2843
  function tryParseJson(str) {
@@ -2959,7 +2965,7 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
2959
2965
  ps = architectSuccess(ps);
2960
2966
  const architectUpdates = {
2961
2967
  _builder: builderBrief,
2962
- _architect: config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
2968
+ _architect: DEFAULT_ARCHITECT_PROMPT,
2963
2969
  _synthesisCount: 0,
2964
2970
  _architectTokens: architectTokens,
2965
2971
  _generatedAt: new Date().toISOString(),
@@ -3031,6 +3037,12 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
3031
3037
  return { executed: true, phaseState: ps, updatedMeta };
3032
3038
  }
3033
3039
  const builderOutput = parseBuilderOutput(rawOutput);
3040
+ if (!builderOutput) {
3041
+ throw new Error('Builder output is not valid JSON — expected { _content|content: string, ... }. ' +
3042
+ 'Raw output (' +
3043
+ rawOutput.length.toString() +
3044
+ ' chars) did not match any JSON extraction strategy.');
3045
+ }
3034
3046
  const builderTokens = result.tokens;
3035
3047
  // Builder success: builder → fresh, critic → pending
3036
3048
  ps = builderSuccess(ps);
@@ -3062,7 +3074,8 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
3062
3074
  try {
3063
3075
  const raw = await readFile(err.outputPath, 'utf8');
3064
3076
  const partial = parseBuilderOutput(raw);
3065
- if (partial.state !== undefined &&
3077
+ if (partial !== null &&
3078
+ partial.state !== undefined &&
3066
3079
  JSON.stringify(partial.state) !== JSON.stringify(currentMeta._state)) {
3067
3080
  partialState = { _state: partial.state };
3068
3081
  }
@@ -3105,7 +3118,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
3105
3118
  const cycleComplete = isFullyFresh(ps);
3106
3119
  const updates = {
3107
3120
  _feedback: feedback,
3108
- _critic: config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
3121
+ _critic: DEFAULT_CRITIC_PROMPT,
3109
3122
  _criticTokens: criticTokens,
3110
3123
  _error: undefined,
3111
3124
  };
@@ -3327,29 +3340,62 @@ function encodePathSegments(p) {
3327
3340
  .map((seg) => encodeURIComponent(seg))
3328
3341
  .join('/');
3329
3342
  }
3343
+ /**
3344
+ * Resolve a filesystem path to a jeeves-server browse path segment.
3345
+ *
3346
+ * - Windows: `j:/domains/foo` → `/j/domains/foo` (drive-letter transform).
3347
+ * - Linux with serverDriveRoots: `/opt/jeeves/content/slack` + roots
3348
+ * `{ content: "/opt/jeeves/content" }` → `/content/slack`.
3349
+ * - Linux without serverDriveRoots: returns the normalized path unchanged
3350
+ * (browse link will be invalid, but this is the pre-existing behavior).
3351
+ */
3352
+ function resolveServerPath(path, serverDriveRoots) {
3353
+ const normalized = normalizePath(path);
3354
+ // Windows: drive letter present — use existing transform
3355
+ if (/^[A-Za-z]:/.test(normalized)) {
3356
+ return normalized.replace(/^([A-Za-z]):/, '/$1');
3357
+ }
3358
+ // Linux: attempt serverDriveRoots resolution (longest/most-specific root wins)
3359
+ if (serverDriveRoots) {
3360
+ const sorted = Object.entries(serverDriveRoots)
3361
+ .map(([label, root]) => [label, normalizePath(root).replace(/\/+$/, '')])
3362
+ .sort((a, b) => b[1].length - a[1].length);
3363
+ for (const [label, normalizedRoot] of sorted) {
3364
+ if (normalized === normalizedRoot ||
3365
+ normalized.startsWith(normalizedRoot + '/')) {
3366
+ const relative = normalized
3367
+ .slice(normalizedRoot.length)
3368
+ .replace(/^\//, '');
3369
+ return `/${label}${relative ? '/' + relative : ''}`;
3370
+ }
3371
+ }
3372
+ }
3373
+ // Fallback: no drive roots configured or no match — return as-is
3374
+ return normalized;
3375
+ }
3330
3376
  /** Build a link (or plain path) to the owner directory. */
3331
- function buildDirectoryLink(path, serverBaseUrl) {
3332
- const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
3333
- const encoded = encodePathSegments(normalized);
3377
+ function buildDirectoryLink(path, serverBaseUrl, serverDriveRoots) {
3378
+ const resolved = resolveServerPath(path, serverDriveRoots);
3379
+ const encoded = encodePathSegments(resolved);
3334
3380
  if (!serverBaseUrl)
3335
- return normalized;
3381
+ return resolved;
3336
3382
  const base = serverBaseUrl.replace(/\/+$/, '');
3337
3383
  return `${base}/path${encoded}`;
3338
3384
  }
3339
3385
  /** Build a link (or plain path) to the entity's meta.json output file. */
3340
- function buildMetaJsonLink(path, serverBaseUrl) {
3341
- const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
3342
- const metaJsonPath = `${normalized}/.meta/meta.json`;
3386
+ function buildMetaJsonLink(path, serverBaseUrl, serverDriveRoots) {
3387
+ const resolved = resolveServerPath(path, serverDriveRoots);
3388
+ const metaJsonPath = `${resolved}/.meta/meta.json`;
3343
3389
  const encoded = encodePathSegments(metaJsonPath);
3344
3390
  if (!serverBaseUrl)
3345
3391
  return metaJsonPath;
3346
3392
  const base = serverBaseUrl.replace(/\/+$/, '');
3347
3393
  return `${base}/path${encoded}`;
3348
3394
  }
3349
- function formatProgressEvent(event, serverBaseUrl) {
3395
+ function formatProgressEvent(event, serverBaseUrl, serverDriveRoots) {
3350
3396
  switch (event.type) {
3351
3397
  case 'synthesis_start': {
3352
- const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
3398
+ const dirLink = buildDirectoryLink(event.path, serverBaseUrl, serverDriveRoots);
3353
3399
  return `🔬 Started meta synthesis: ${dirLink}`;
3354
3400
  }
3355
3401
  case 'phase_start': {
@@ -3365,7 +3411,7 @@ function formatProgressEvent(event, serverBaseUrl) {
3365
3411
  return ` ✅ ${phase} complete (${tokenStr} / ${duration})`;
3366
3412
  }
3367
3413
  case 'synthesis_complete': {
3368
- const metaLink = buildMetaJsonLink(event.path, serverBaseUrl);
3414
+ const metaLink = buildMetaJsonLink(event.path, serverBaseUrl, serverDriveRoots);
3369
3415
  const tokenStr = formatTokens(event.tokens);
3370
3416
  const duration = event.durationMs !== undefined
3371
3417
  ? formatSeconds(event.durationMs)
@@ -3373,7 +3419,7 @@ function formatProgressEvent(event, serverBaseUrl) {
3373
3419
  return `✅ Completed: ${metaLink} (${tokenStr} / ${duration})`;
3374
3420
  }
3375
3421
  case 'error': {
3376
- const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
3422
+ const dirLink = buildDirectoryLink(event.path, serverBaseUrl, serverDriveRoots);
3377
3423
  const phase = event.phase ? `${titleCasePhase(event.phase)} ` : '';
3378
3424
  const error = event.error ?? 'Unknown error';
3379
3425
  return `❌ Synthesis failed at ${phase}phase: ${dirLink}\n Error: ${error}`;
@@ -3396,7 +3442,7 @@ class ProgressReporter {
3396
3442
  const target = this.config.reportTarget ?? this.config.reportChannel;
3397
3443
  if (!target)
3398
3444
  return;
3399
- const message = formatProgressEvent(event, this.config.serverBaseUrl);
3445
+ const message = formatProgressEvent(event, this.config.serverBaseUrl, this.config.serverDriveRoots);
3400
3446
  const url = new URL('/tools/invoke', this.config.gatewayUrl);
3401
3447
  const args = {
3402
3448
  action: 'send',
@@ -4710,9 +4756,10 @@ function registerPreviewRoute(app, deps) {
4710
4756
  ownedFiles: scopeFiles.length,
4711
4757
  childMetas: targetNode.children.length,
4712
4758
  deltaFiles: deltaFiles
4713
- .slice(0, 50)
4759
+ .slice(0, config.previewDeltaFilesCap)
4714
4760
  .map((f) => ({ path: f, action: 'modified' })),
4715
4761
  deltaCount: deltaFiles.length,
4762
+ deltaFilesTruncated: deltaFiles.length > config.previewDeltaFilesCap,
4716
4763
  },
4717
4764
  estimatedTokens,
4718
4765
  // New phase-state fields (additive)
@@ -5386,6 +5433,9 @@ async function startService(config, configPath) {
5386
5433
  const executor = new GatewayExecutor({
5387
5434
  gatewayUrl: config.gatewayUrl,
5388
5435
  apiKey: config.gatewayApiKey,
5436
+ workspaceDir: config.workspaceDir,
5437
+ stagingRetries: config.stagingRetries,
5438
+ stagingRetryDelayMs: config.stagingRetryDelayMs,
5389
5439
  });
5390
5440
  // Runtime stats (mutable, shared with routes)
5391
5441
  const stats = {
@@ -26,12 +26,13 @@ export declare function parseArchitectOutput(output: string): string;
26
26
  /**
27
27
  * Parse builder output. The builder returns JSON with _content and optional fields.
28
28
  *
29
- * Attempts JSON parse first. If that fails, treats the entire output as _content.
29
+ * Attempts JSON extraction via multiple strategies. Returns null if all strategies fail
30
+ * (e.g. the output is plain text, self-talk, or a sub-agent delegation response).
30
31
  *
31
32
  * @param output - Raw subprocess output.
32
- * @returns Parsed builder output with content and structured fields.
33
+ * @returns Parsed builder output, or null if output is not valid builder JSON.
33
34
  */
34
- export declare function parseBuilderOutput(output: string): BuilderOutput;
35
+ export declare function parseBuilderOutput(output: string): BuilderOutput | null;
35
36
  /**
36
37
  * Parse critic output. The critic returns evaluation text.
37
38
  *
@@ -28,8 +28,14 @@ export type ProgressReporterConfig = {
28
28
  reportTarget?: string;
29
29
  /** Optional base URL for the service, used to construct entity links. */
30
30
  serverBaseUrl?: string;
31
+ /**
32
+ * Optional mapping of server drive labels to absolute filesystem paths.
33
+ * Used on Linux to resolve absolute paths to jeeves-server browse paths.
34
+ * Example: `{ "content": "/opt/jeeves/content" }`
35
+ */
36
+ serverDriveRoots?: Record<string, string>;
31
37
  };
32
- export declare function formatProgressEvent(event: ProgressEvent, serverBaseUrl?: string): string;
38
+ export declare function formatProgressEvent(event: ProgressEvent, serverBaseUrl?: string, serverDriveRoots?: Record<string, string>): string;
33
39
  export declare class ProgressReporter {
34
40
  private readonly config;
35
41
  private readonly logger;
@@ -4,9 +4,6 @@
4
4
  * Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
5
5
  * Loaded at runtime relative to the compiled module location.
6
6
  *
7
- * Users can override via `defaultArchitect` / `defaultCritic` in the service
8
- * config. Most installations should use the built-in defaults.
9
- *
10
7
  * @module prompts
11
8
  */
12
9
  /** Built-in default architect prompt. */
@@ -27,16 +27,15 @@ export declare const serviceConfigSchema: z.ZodObject<{
27
27
  maxArchive: z.ZodDefault<z.ZodNumber>;
28
28
  maxLines: z.ZodDefault<z.ZodNumber>;
29
29
  thinking: z.ZodDefault<z.ZodString>;
30
- defaultArchitect: z.ZodOptional<z.ZodString>;
31
- defaultCritic: z.ZodOptional<z.ZodString>;
32
30
  skipUnchanged: z.ZodDefault<z.ZodBoolean>;
33
- metaProperty: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
34
- metaArchiveProperty: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
31
+ metaProperty: z.ZodRecord<z.ZodString, z.ZodUnknown>;
32
+ metaArchiveProperty: z.ZodRecord<z.ZodString, z.ZodUnknown>;
35
33
  port: z.ZodDefault<z.ZodNumber>;
36
34
  schedule: z.ZodDefault<z.ZodString>;
37
35
  reportChannel: z.ZodOptional<z.ZodString>;
38
36
  reportTarget: z.ZodOptional<z.ZodString>;
39
37
  serverBaseUrl: z.ZodOptional<z.ZodString>;
38
+ serverDriveRoots: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
40
39
  watcherHealthIntervalMs: z.ZodDefault<z.ZodNumber>;
41
40
  logging: z.ZodDefault<z.ZodObject<{
42
41
  level: z.ZodDefault<z.ZodString>;
@@ -49,6 +48,10 @@ export declare const serviceConfigSchema: z.ZodObject<{
49
48
  crossRefs: z.ZodOptional<z.ZodArray<z.ZodString>>;
50
49
  parentDepth: z.ZodOptional<z.ZodNumber>;
51
50
  }, z.core.$strip>>>>;
51
+ workspaceDir: z.ZodOptional<z.ZodString>;
52
+ stagingRetries: z.ZodDefault<z.ZodNumber>;
53
+ stagingRetryDelayMs: z.ZodDefault<z.ZodNumber>;
54
+ previewDeltaFilesCap: z.ZodDefault<z.ZodNumber>;
52
55
  }, z.core.$strip>;
53
56
  /** Inferred type for service configuration. */
54
57
  export type ServiceConfig = z.infer<typeof serviceConfigSchema>;
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@karmaniverous/jeeves": "^0.5.12",
11
- "@karmaniverous/jeeves-meta-core": "^0.2.0",
11
+ "@karmaniverous/jeeves-meta-core": "^0.2.2",
12
12
  "commander": "^14",
13
13
  "croner": "^10",
14
14
  "fastify": "^5.8",
@@ -110,5 +110,5 @@
110
110
  },
111
111
  "type": "module",
112
112
  "types": "dist/index.d.ts",
113
- "version": "0.16.1"
113
+ "version": "0.16.3"
114
114
  }