@karmaniverous/jeeves-meta 0.16.2 → 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
@@ -19,7 +19,7 @@ HTTP service for the Jeeves knowledge synthesis engine. Provides a Fastify API,
19
19
  - **Virtual rule registration** — registers 3 watcher inference rules at startup with retry
20
20
  - **Progress reporting** — real-time synthesis events via gateway channel messages
21
21
  - **Graceful shutdown** — stop scheduler, release locks, close server
22
- - **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
23
23
  - **Handlebars templates** — prompts compiled with `{ config, meta, scope }` context; architect can write template expressions into builder briefs
24
24
  - **Config hot-reload** — all synthesis parameters reload without restart; restart-required fields (port, URLs) warn on change
25
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. */
@@ -819,7 +823,7 @@ const serviceConfigSchema = metaConfigSchema.extend({
819
823
  /**
820
824
  * Load and resolve jeeves-meta service config.
821
825
  *
822
- * Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
826
+ * Supports environment-variable substitution (dollar-brace pattern).
823
827
  *
824
828
  * @module configLoader
825
829
  */
@@ -851,24 +855,10 @@ function substituteEnvVars(value) {
851
855
  }
852
856
  return value;
853
857
  }
854
- /**
855
- * Resolve \@file: references in a config value.
856
- *
857
- * @param value - String value that may start with "\@file:".
858
- * @param baseDir - Base directory for resolving relative paths.
859
- * @returns The resolved string (file contents or original value).
860
- */
861
- function resolveFileRef(value, baseDir) {
862
- if (!value.startsWith('@file:'))
863
- return value;
864
- const filePath = join(baseDir, value.slice(6));
865
- return readFileSync(filePath, 'utf8');
866
- }
867
858
  /**
868
859
  * Load service config from a JSON file.
869
860
  *
870
- * Resolves \@file: references for defaultArchitect and defaultCritic,
871
- * and substitutes environment-variable placeholders throughout.
861
+ * Substitutes environment-variable placeholders throughout.
872
862
  *
873
863
  * @param configPath - Path to config JSON file.
874
864
  * @returns Validated ServiceConfig.
@@ -876,13 +866,6 @@ function resolveFileRef(value, baseDir) {
876
866
  function loadServiceConfig(configPath) {
877
867
  const rawText = readFileSync(configPath, 'utf8');
878
868
  const raw = substituteEnvVars(JSON.parse(rawText));
879
- const baseDir = dirname(configPath);
880
- if (typeof raw['defaultArchitect'] === 'string') {
881
- raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
882
- }
883
- if (typeof raw['defaultCritic'] === 'string') {
884
- raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
885
- }
886
869
  return serviceConfigSchema.parse(raw);
887
870
  }
888
871
 
@@ -1346,9 +1329,6 @@ function packageDirectorySync({cwd, ignoreTypeOnlyPackageJson} = {}) {
1346
1329
  * Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
1347
1330
  * Loaded at runtime relative to the compiled module location.
1348
1331
  *
1349
- * Users can override via `defaultArchitect` / `defaultCritic` in the service
1350
- * config. Most installations should use the built-in defaults.
1351
- *
1352
1332
  * @module prompts
1353
1333
  */
1354
1334
  const packageRoot = packageDirectorySync({
@@ -1836,7 +1816,7 @@ function buildArchitectTask(ctx, meta, config) {
1836
1816
  const sections = [
1837
1817
  `# jeeves-meta · ARCHITECT · ${ctx.path}`,
1838
1818
  '',
1839
- config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
1819
+ DEFAULT_ARCHITECT_PROMPT,
1840
1820
  '',
1841
1821
  '## SCOPE',
1842
1822
  `Path: ${ctx.path}`,
@@ -1885,7 +1865,7 @@ function buildBuilderTask(ctx, meta, config) {
1885
1865
  includeSteer: false,
1886
1866
  feedbackHeading: '## FEEDBACK FROM CRITIC',
1887
1867
  });
1888
- 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.');
1889
1869
  return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
1890
1870
  }
1891
1871
  /**
@@ -1900,7 +1880,7 @@ function buildCriticTask(ctx, meta, config) {
1900
1880
  const sections = [
1901
1881
  `# jeeves-meta · CRITIC · ${ctx.path}`,
1902
1882
  '',
1903
- config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
1883
+ DEFAULT_CRITIC_PROMPT,
1904
1884
  '',
1905
1885
  '## SYNTHESIS TO EVALUATE',
1906
1886
  meta._content ?? '(No content produced)',
@@ -2317,8 +2297,8 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
2317
2297
  // through real builder cycles, architect runs with the current prompt and
2318
2298
  // the snapshot updates. Coupling prompt changes to invalidation causes a
2319
2299
  // corpus-wide synthesis storm (see #163).
2320
- const architectChanged = isPromptStale(meta._architect, config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT);
2321
- 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);
2322
2302
  const effectiveSynthesisCount = meta._synthesisCount ?? 0;
2323
2303
  // _crossRefs declaration change
2324
2304
  const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
@@ -2565,10 +2545,11 @@ function parseArchitectOutput(output) {
2565
2545
  /**
2566
2546
  * Parse builder output. The builder returns JSON with _content and optional fields.
2567
2547
  *
2568
- * 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).
2569
2550
  *
2570
2551
  * @param output - Raw subprocess output.
2571
- * @returns Parsed builder output with content and structured fields.
2552
+ * @returns Parsed builder output, or null if output is not valid builder JSON.
2572
2553
  */
2573
2554
  function parseBuilderOutput(output) {
2574
2555
  const trimmed = stripSentinel(output);
@@ -2597,8 +2578,8 @@ function parseBuilderOutput(output) {
2597
2578
  if (result)
2598
2579
  return result;
2599
2580
  }
2600
- // Fallback: treat entire output as content
2601
- return { content: trimmed, fields: {} };
2581
+ // All JSON strategies failed not valid builder output
2582
+ return null;
2602
2583
  }
2603
2584
  /** Try to parse a string as JSON and extract builder output fields. */
2604
2585
  function tryParseJson(str) {
@@ -2726,7 +2707,7 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
2726
2707
  ps = architectSuccess(ps);
2727
2708
  const architectUpdates = {
2728
2709
  _builder: builderBrief,
2729
- _architect: config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
2710
+ _architect: DEFAULT_ARCHITECT_PROMPT,
2730
2711
  _synthesisCount: 0,
2731
2712
  _architectTokens: architectTokens,
2732
2713
  _generatedAt: new Date().toISOString(),
@@ -2798,6 +2779,12 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
2798
2779
  return { executed: true, phaseState: ps, updatedMeta };
2799
2780
  }
2800
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
+ }
2801
2788
  const builderTokens = result.tokens;
2802
2789
  // Builder success: builder → fresh, critic → pending
2803
2790
  ps = builderSuccess(ps);
@@ -2829,7 +2816,8 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
2829
2816
  try {
2830
2817
  const raw = await readFile(err.outputPath, 'utf8');
2831
2818
  const partial = parseBuilderOutput(raw);
2832
- if (partial.state !== undefined &&
2819
+ if (partial !== null &&
2820
+ partial.state !== undefined &&
2833
2821
  JSON.stringify(partial.state) !== JSON.stringify(currentMeta._state)) {
2834
2822
  partialState = { _state: partial.state };
2835
2823
  }
@@ -2872,7 +2860,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
2872
2860
  const cycleComplete = isFullyFresh(ps);
2873
2861
  const updates = {
2874
2862
  _feedback: feedback,
2875
- _critic: config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
2863
+ _critic: DEFAULT_CRITIC_PROMPT,
2876
2864
  _criticTokens: criticTokens,
2877
2865
  _error: undefined,
2878
2866
  };
@@ -3094,29 +3082,62 @@ function encodePathSegments(p) {
3094
3082
  .map((seg) => encodeURIComponent(seg))
3095
3083
  .join('/');
3096
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
+ }
3097
3118
  /** Build a link (or plain path) to the owner directory. */
3098
- function buildDirectoryLink(path, serverBaseUrl) {
3099
- const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
3100
- const encoded = encodePathSegments(normalized);
3119
+ function buildDirectoryLink(path, serverBaseUrl, serverDriveRoots) {
3120
+ const resolved = resolveServerPath(path, serverDriveRoots);
3121
+ const encoded = encodePathSegments(resolved);
3101
3122
  if (!serverBaseUrl)
3102
- return normalized;
3123
+ return resolved;
3103
3124
  const base = serverBaseUrl.replace(/\/+$/, '');
3104
3125
  return `${base}/path${encoded}`;
3105
3126
  }
3106
3127
  /** Build a link (or plain path) to the entity's meta.json output file. */
3107
- function buildMetaJsonLink(path, serverBaseUrl) {
3108
- const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
3109
- 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`;
3110
3131
  const encoded = encodePathSegments(metaJsonPath);
3111
3132
  if (!serverBaseUrl)
3112
3133
  return metaJsonPath;
3113
3134
  const base = serverBaseUrl.replace(/\/+$/, '');
3114
3135
  return `${base}/path${encoded}`;
3115
3136
  }
3116
- function formatProgressEvent(event, serverBaseUrl) {
3137
+ function formatProgressEvent(event, serverBaseUrl, serverDriveRoots) {
3117
3138
  switch (event.type) {
3118
3139
  case 'synthesis_start': {
3119
- const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
3140
+ const dirLink = buildDirectoryLink(event.path, serverBaseUrl, serverDriveRoots);
3120
3141
  return `🔬 Started meta synthesis: ${dirLink}`;
3121
3142
  }
3122
3143
  case 'phase_start': {
@@ -3132,7 +3153,7 @@ function formatProgressEvent(event, serverBaseUrl) {
3132
3153
  return ` ✅ ${phase} complete (${tokenStr} / ${duration})`;
3133
3154
  }
3134
3155
  case 'synthesis_complete': {
3135
- const metaLink = buildMetaJsonLink(event.path, serverBaseUrl);
3156
+ const metaLink = buildMetaJsonLink(event.path, serverBaseUrl, serverDriveRoots);
3136
3157
  const tokenStr = formatTokens(event.tokens);
3137
3158
  const duration = event.durationMs !== undefined
3138
3159
  ? formatSeconds(event.durationMs)
@@ -3140,7 +3161,7 @@ function formatProgressEvent(event, serverBaseUrl) {
3140
3161
  return `✅ Completed: ${metaLink} (${tokenStr} / ${duration})`;
3141
3162
  }
3142
3163
  case 'error': {
3143
- const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
3164
+ const dirLink = buildDirectoryLink(event.path, serverBaseUrl, serverDriveRoots);
3144
3165
  const phase = event.phase ? `${titleCasePhase(event.phase)} ` : '';
3145
3166
  const error = event.error ?? 'Unknown error';
3146
3167
  return `❌ Synthesis failed at ${phase}phase: ${dirLink}\n Error: ${error}`;
@@ -3163,7 +3184,7 @@ class ProgressReporter {
3163
3184
  const target = this.config.reportTarget ?? this.config.reportChannel;
3164
3185
  if (!target)
3165
3186
  return;
3166
- const message = formatProgressEvent(event, this.config.serverBaseUrl);
3187
+ const message = formatProgressEvent(event, this.config.serverBaseUrl, this.config.serverDriveRoots);
3167
3188
  const url = new URL('/tools/invoke', this.config.gatewayUrl);
3168
3189
  const args = {
3169
3190
  action: 'send',
@@ -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.
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. */
@@ -1308,7 +1312,7 @@ const serviceConfigSchema = metaConfigSchema.extend({
1308
1312
  /**
1309
1313
  * Load and resolve jeeves-meta service config.
1310
1314
  *
1311
- * Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
1315
+ * Supports environment-variable substitution (dollar-brace pattern).
1312
1316
  *
1313
1317
  * @module configLoader
1314
1318
  */
@@ -1340,19 +1344,6 @@ function substituteEnvVars(value) {
1340
1344
  }
1341
1345
  return value;
1342
1346
  }
1343
- /**
1344
- * Resolve \@file: references in a config value.
1345
- *
1346
- * @param value - String value that may start with "\@file:".
1347
- * @param baseDir - Base directory for resolving relative paths.
1348
- * @returns The resolved string (file contents or original value).
1349
- */
1350
- function resolveFileRef(value, baseDir) {
1351
- if (!value.startsWith('@file:'))
1352
- return value;
1353
- const filePath = join(baseDir, value.slice(6));
1354
- return readFileSync(filePath, 'utf8');
1355
- }
1356
1347
  /**
1357
1348
  * Migrate legacy config path to the new canonical location.
1358
1349
  *
@@ -1401,8 +1392,7 @@ function resolveConfigPath(args) {
1401
1392
  /**
1402
1393
  * Load service config from a JSON file.
1403
1394
  *
1404
- * Resolves \@file: references for defaultArchitect and defaultCritic,
1405
- * and substitutes environment-variable placeholders throughout.
1395
+ * Substitutes environment-variable placeholders throughout.
1406
1396
  *
1407
1397
  * @param configPath - Path to config JSON file.
1408
1398
  * @returns Validated ServiceConfig.
@@ -1410,13 +1400,6 @@ function resolveConfigPath(args) {
1410
1400
  function loadServiceConfig(configPath) {
1411
1401
  const rawText = readFileSync(configPath, 'utf8');
1412
1402
  const raw = substituteEnvVars(JSON.parse(rawText));
1413
- const baseDir = dirname(configPath);
1414
- if (typeof raw['defaultArchitect'] === 'string') {
1415
- raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
1416
- }
1417
- if (typeof raw['defaultCritic'] === 'string') {
1418
- raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
1419
- }
1420
1403
  return serviceConfigSchema.parse(raw);
1421
1404
  }
1422
1405
 
@@ -1792,9 +1775,6 @@ function createLogger(config) {
1792
1775
  * Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
1793
1776
  * Loaded at runtime relative to the compiled module location.
1794
1777
  *
1795
- * Users can override via `defaultArchitect` / `defaultCritic` in the service
1796
- * config. Most installations should use the built-in defaults.
1797
- *
1798
1778
  * @module prompts
1799
1779
  */
1800
1780
  const packageRoot = packageDirectorySync({
@@ -2094,7 +2074,7 @@ function buildArchitectTask(ctx, meta, config) {
2094
2074
  const sections = [
2095
2075
  `# jeeves-meta · ARCHITECT · ${ctx.path}`,
2096
2076
  '',
2097
- config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
2077
+ DEFAULT_ARCHITECT_PROMPT,
2098
2078
  '',
2099
2079
  '## SCOPE',
2100
2080
  `Path: ${ctx.path}`,
@@ -2143,7 +2123,7 @@ function buildBuilderTask(ctx, meta, config) {
2143
2123
  includeSteer: false,
2144
2124
  feedbackHeading: '## FEEDBACK FROM CRITIC',
2145
2125
  });
2146
- 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.');
2147
2127
  return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
2148
2128
  }
2149
2129
  /**
@@ -2158,7 +2138,7 @@ function buildCriticTask(ctx, meta, config) {
2158
2138
  const sections = [
2159
2139
  `# jeeves-meta · CRITIC · ${ctx.path}`,
2160
2140
  '',
2161
- config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
2141
+ DEFAULT_CRITIC_PROMPT,
2162
2142
  '',
2163
2143
  '## SYNTHESIS TO EVALUATE',
2164
2144
  meta._content ?? '(No content produced)',
@@ -2575,8 +2555,8 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
2575
2555
  // through real builder cycles, architect runs with the current prompt and
2576
2556
  // the snapshot updates. Coupling prompt changes to invalidation causes a
2577
2557
  // corpus-wide synthesis storm (see #163).
2578
- const architectChanged = isPromptStale(meta._architect, config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT);
2579
- 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);
2580
2560
  const effectiveSynthesisCount = meta._synthesisCount ?? 0;
2581
2561
  // _crossRefs declaration change
2582
2562
  const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
@@ -2823,10 +2803,11 @@ function parseArchitectOutput(output) {
2823
2803
  /**
2824
2804
  * Parse builder output. The builder returns JSON with _content and optional fields.
2825
2805
  *
2826
- * 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).
2827
2808
  *
2828
2809
  * @param output - Raw subprocess output.
2829
- * @returns Parsed builder output with content and structured fields.
2810
+ * @returns Parsed builder output, or null if output is not valid builder JSON.
2830
2811
  */
2831
2812
  function parseBuilderOutput(output) {
2832
2813
  const trimmed = stripSentinel(output);
@@ -2855,8 +2836,8 @@ function parseBuilderOutput(output) {
2855
2836
  if (result)
2856
2837
  return result;
2857
2838
  }
2858
- // Fallback: treat entire output as content
2859
- return { content: trimmed, fields: {} };
2839
+ // All JSON strategies failed not valid builder output
2840
+ return null;
2860
2841
  }
2861
2842
  /** Try to parse a string as JSON and extract builder output fields. */
2862
2843
  function tryParseJson(str) {
@@ -2984,7 +2965,7 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
2984
2965
  ps = architectSuccess(ps);
2985
2966
  const architectUpdates = {
2986
2967
  _builder: builderBrief,
2987
- _architect: config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
2968
+ _architect: DEFAULT_ARCHITECT_PROMPT,
2988
2969
  _synthesisCount: 0,
2989
2970
  _architectTokens: architectTokens,
2990
2971
  _generatedAt: new Date().toISOString(),
@@ -3056,6 +3037,12 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
3056
3037
  return { executed: true, phaseState: ps, updatedMeta };
3057
3038
  }
3058
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
+ }
3059
3046
  const builderTokens = result.tokens;
3060
3047
  // Builder success: builder → fresh, critic → pending
3061
3048
  ps = builderSuccess(ps);
@@ -3087,7 +3074,8 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
3087
3074
  try {
3088
3075
  const raw = await readFile(err.outputPath, 'utf8');
3089
3076
  const partial = parseBuilderOutput(raw);
3090
- if (partial.state !== undefined &&
3077
+ if (partial !== null &&
3078
+ partial.state !== undefined &&
3091
3079
  JSON.stringify(partial.state) !== JSON.stringify(currentMeta._state)) {
3092
3080
  partialState = { _state: partial.state };
3093
3081
  }
@@ -3130,7 +3118,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
3130
3118
  const cycleComplete = isFullyFresh(ps);
3131
3119
  const updates = {
3132
3120
  _feedback: feedback,
3133
- _critic: config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
3121
+ _critic: DEFAULT_CRITIC_PROMPT,
3134
3122
  _criticTokens: criticTokens,
3135
3123
  _error: undefined,
3136
3124
  };
@@ -3352,29 +3340,62 @@ function encodePathSegments(p) {
3352
3340
  .map((seg) => encodeURIComponent(seg))
3353
3341
  .join('/');
3354
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
+ }
3355
3376
  /** Build a link (or plain path) to the owner directory. */
3356
- function buildDirectoryLink(path, serverBaseUrl) {
3357
- const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
3358
- const encoded = encodePathSegments(normalized);
3377
+ function buildDirectoryLink(path, serverBaseUrl, serverDriveRoots) {
3378
+ const resolved = resolveServerPath(path, serverDriveRoots);
3379
+ const encoded = encodePathSegments(resolved);
3359
3380
  if (!serverBaseUrl)
3360
- return normalized;
3381
+ return resolved;
3361
3382
  const base = serverBaseUrl.replace(/\/+$/, '');
3362
3383
  return `${base}/path${encoded}`;
3363
3384
  }
3364
3385
  /** Build a link (or plain path) to the entity's meta.json output file. */
3365
- function buildMetaJsonLink(path, serverBaseUrl) {
3366
- const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
3367
- 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`;
3368
3389
  const encoded = encodePathSegments(metaJsonPath);
3369
3390
  if (!serverBaseUrl)
3370
3391
  return metaJsonPath;
3371
3392
  const base = serverBaseUrl.replace(/\/+$/, '');
3372
3393
  return `${base}/path${encoded}`;
3373
3394
  }
3374
- function formatProgressEvent(event, serverBaseUrl) {
3395
+ function formatProgressEvent(event, serverBaseUrl, serverDriveRoots) {
3375
3396
  switch (event.type) {
3376
3397
  case 'synthesis_start': {
3377
- const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
3398
+ const dirLink = buildDirectoryLink(event.path, serverBaseUrl, serverDriveRoots);
3378
3399
  return `🔬 Started meta synthesis: ${dirLink}`;
3379
3400
  }
3380
3401
  case 'phase_start': {
@@ -3390,7 +3411,7 @@ function formatProgressEvent(event, serverBaseUrl) {
3390
3411
  return ` ✅ ${phase} complete (${tokenStr} / ${duration})`;
3391
3412
  }
3392
3413
  case 'synthesis_complete': {
3393
- const metaLink = buildMetaJsonLink(event.path, serverBaseUrl);
3414
+ const metaLink = buildMetaJsonLink(event.path, serverBaseUrl, serverDriveRoots);
3394
3415
  const tokenStr = formatTokens(event.tokens);
3395
3416
  const duration = event.durationMs !== undefined
3396
3417
  ? formatSeconds(event.durationMs)
@@ -3398,7 +3419,7 @@ function formatProgressEvent(event, serverBaseUrl) {
3398
3419
  return `✅ Completed: ${metaLink} (${tokenStr} / ${duration})`;
3399
3420
  }
3400
3421
  case 'error': {
3401
- const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
3422
+ const dirLink = buildDirectoryLink(event.path, serverBaseUrl, serverDriveRoots);
3402
3423
  const phase = event.phase ? `${titleCasePhase(event.phase)} ` : '';
3403
3424
  const error = event.error ?? 'Unknown error';
3404
3425
  return `❌ Synthesis failed at ${phase}phase: ${dirLink}\n Error: ${error}`;
@@ -3421,7 +3442,7 @@ class ProgressReporter {
3421
3442
  const target = this.config.reportTarget ?? this.config.reportChannel;
3422
3443
  if (!target)
3423
3444
  return;
3424
- const message = formatProgressEvent(event, this.config.serverBaseUrl);
3445
+ const message = formatProgressEvent(event, this.config.serverBaseUrl, this.config.serverDriveRoots);
3425
3446
  const url = new URL('/tools/invoke', this.config.gatewayUrl);
3426
3447
  const args = {
3427
3448
  action: 'send',
@@ -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>;
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.1",
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.2"
113
+ "version": "0.16.3"
114
114
  }