@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 +3 -1
- package/dist/cli/jeeves-meta/index.js +104 -54
- package/dist/configHotReload.d.ts +1 -1
- package/dist/configLoader.d.ts +2 -3
- package/dist/executor/GatewayExecutor.d.ts +6 -0
- package/dist/index.js +104 -54
- package/dist/orchestrator/parseOutput.d.ts +4 -3
- package/dist/progress/index.d.ts +7 -1
- package/dist/prompts/index.d.ts +0 -3
- package/dist/schema/config.d.ts +7 -4
- package/package.json +2 -2
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
2296
|
-
const criticChanged = isPromptStale(meta._critic,
|
|
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
|
|
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
|
|
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
|
-
//
|
|
2576
|
-
return
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
3075
|
-
const encoded = encodePathSegments(
|
|
3119
|
+
function buildDirectoryLink(path, serverBaseUrl, serverDriveRoots) {
|
|
3120
|
+
const resolved = resolveServerPath(path, serverDriveRoots);
|
|
3121
|
+
const encoded = encodePathSegments(resolved);
|
|
3076
3122
|
if (!serverBaseUrl)
|
|
3077
|
-
return
|
|
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
|
|
3084
|
-
const metaJsonPath = `${
|
|
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,
|
|
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"
|
|
18
|
+
export declare const RESTART_REQUIRED_FIELDS: readonly ["port", "watcherUrl", "gatewayUrl", "gatewayApiKey"];
|
|
19
19
|
interface ConfigHotReloadRuntime {
|
|
20
20
|
config: ServiceConfig;
|
|
21
21
|
logger: Logger;
|
package/dist/configLoader.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Load and resolve jeeves-meta service config.
|
|
3
3
|
*
|
|
4
|
-
* Supports
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
2554
|
-
const criticChanged = isPromptStale(meta._critic,
|
|
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
|
|
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
|
|
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
|
-
//
|
|
2834
|
-
return
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
3333
|
-
const encoded = encodePathSegments(
|
|
3377
|
+
function buildDirectoryLink(path, serverBaseUrl, serverDriveRoots) {
|
|
3378
|
+
const resolved = resolveServerPath(path, serverDriveRoots);
|
|
3379
|
+
const encoded = encodePathSegments(resolved);
|
|
3334
3380
|
if (!serverBaseUrl)
|
|
3335
|
-
return
|
|
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
|
|
3342
|
-
const metaJsonPath = `${
|
|
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,
|
|
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
|
|
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
|
|
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
|
*
|
package/dist/progress/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/prompts/index.d.ts
CHANGED
|
@@ -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. */
|
package/dist/schema/config.d.ts
CHANGED
|
@@ -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.
|
|
34
|
-
metaArchiveProperty: z.
|
|
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.
|
|
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.
|
|
113
|
+
"version": "0.16.3"
|
|
114
114
|
}
|