@karmaniverous/jeeves-meta 0.16.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/jeeves-meta/index.js +177 -157
- package/dist/configHotReload.d.ts +1 -1
- package/dist/configLoader.d.ts +2 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +178 -158
- package/dist/orchestrator/parseOutput.d.ts +4 -3
- package/dist/progress/index.d.ts +39 -4
- package/dist/prompts/index.d.ts +0 -3
- package/dist/schema/config.d.ts +14 -5
- package/dist/schema/index.d.ts +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { sleepAsync, createConfigQueryHandler, atomicWrite, createStatusHandler, getBindAddress, fetchJson, postJson, getServiceUrl, jeevesComponentDescriptorSchema, createServiceCli } from '@karmaniverous/jeeves';
|
|
2
|
+
import { sleepAsync, fetchWithTimeout, createConfigQueryHandler, atomicWrite, createStatusHandler, getBindAddress, fetchJson, postJson, getServiceUrl, jeevesComponentDescriptorSchema, createServiceCli } from '@karmaniverous/jeeves';
|
|
3
3
|
import { normalizePath, metaConfigSchema, getEndpoint, META_COMPONENT } from '@karmaniverous/jeeves-meta-core';
|
|
4
4
|
import fs, { unlinkSync, writeFileSync, existsSync, readFileSync, statSync, mkdirSync, readdirSync, watchFile } from 'node:fs';
|
|
5
5
|
import path, { join, dirname, relative, posix, resolve } from 'node:path';
|
|
@@ -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. */
|
|
@@ -765,6 +763,12 @@ function applyHotReloadedConfig(newConfig) {
|
|
|
765
763
|
*
|
|
766
764
|
* @module schema/config
|
|
767
765
|
*/
|
|
766
|
+
/** Default Handlebars template strings for progress reporting. Single source of truth. */
|
|
767
|
+
const DEFAULT_TEMPLATE_STRINGS = {
|
|
768
|
+
phaseStart: ':gear: Started meta synthesis {{phase}} phase of <{{dirLink}}>',
|
|
769
|
+
phaseEnd: ':white_check_mark: Completed meta synthesis {{phase}} phase ({{tokens}} tokens / {{seconds}}s) at <{{metaLink}}>',
|
|
770
|
+
phaseError: ':x: Meta synthesis {{phase}} phase failed at <{{dirLink}}>\n Error: {{error}}',
|
|
771
|
+
};
|
|
768
772
|
/** Zod schema for logging configuration. */
|
|
769
773
|
const loggingSchema = z.object({
|
|
770
774
|
/** Log level. */
|
|
@@ -793,8 +797,24 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
793
797
|
reportChannel: z.string().optional(),
|
|
794
798
|
/** Channel/user ID to send progress messages to. */
|
|
795
799
|
reportTarget: z.string().optional(),
|
|
796
|
-
/**
|
|
797
|
-
|
|
800
|
+
/**
|
|
801
|
+
* URL of the local jeeves-server instance, used by the ProgressReporter
|
|
802
|
+
* to resolve filesystem paths to browse links via the resolve-path API.
|
|
803
|
+
* Default: http://127.0.0.1:1934
|
|
804
|
+
*/
|
|
805
|
+
serverUrl: z.string().default('http://127.0.0.1:1934'),
|
|
806
|
+
/**
|
|
807
|
+
* Handlebars templates for progress reporting messages.
|
|
808
|
+
* Each template receives a standard set of data keys (dirLink, metaLink,
|
|
809
|
+
* phase, tokens, seconds, error).
|
|
810
|
+
*/
|
|
811
|
+
templates: z
|
|
812
|
+
.object({
|
|
813
|
+
phaseStart: z.string().default(DEFAULT_TEMPLATE_STRINGS.phaseStart),
|
|
814
|
+
phaseEnd: z.string().default(DEFAULT_TEMPLATE_STRINGS.phaseEnd),
|
|
815
|
+
phaseError: z.string().default(DEFAULT_TEMPLATE_STRINGS.phaseError),
|
|
816
|
+
})
|
|
817
|
+
.default(() => ({ ...DEFAULT_TEMPLATE_STRINGS })),
|
|
798
818
|
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
799
819
|
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
800
820
|
/** Logging configuration. */
|
|
@@ -819,7 +839,7 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
819
839
|
/**
|
|
820
840
|
* Load and resolve jeeves-meta service config.
|
|
821
841
|
*
|
|
822
|
-
* Supports
|
|
842
|
+
* Supports environment-variable substitution (dollar-brace pattern).
|
|
823
843
|
*
|
|
824
844
|
* @module configLoader
|
|
825
845
|
*/
|
|
@@ -851,24 +871,10 @@ function substituteEnvVars(value) {
|
|
|
851
871
|
}
|
|
852
872
|
return value;
|
|
853
873
|
}
|
|
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
874
|
/**
|
|
868
875
|
* Load service config from a JSON file.
|
|
869
876
|
*
|
|
870
|
-
*
|
|
871
|
-
* and substitutes environment-variable placeholders throughout.
|
|
877
|
+
* Substitutes environment-variable placeholders throughout.
|
|
872
878
|
*
|
|
873
879
|
* @param configPath - Path to config JSON file.
|
|
874
880
|
* @returns Validated ServiceConfig.
|
|
@@ -876,13 +882,6 @@ function resolveFileRef(value, baseDir) {
|
|
|
876
882
|
function loadServiceConfig(configPath) {
|
|
877
883
|
const rawText = readFileSync(configPath, 'utf8');
|
|
878
884
|
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
885
|
return serviceConfigSchema.parse(raw);
|
|
887
886
|
}
|
|
888
887
|
|
|
@@ -1346,9 +1345,6 @@ function packageDirectorySync({cwd, ignoreTypeOnlyPackageJson} = {}) {
|
|
|
1346
1345
|
* Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
|
|
1347
1346
|
* Loaded at runtime relative to the compiled module location.
|
|
1348
1347
|
*
|
|
1349
|
-
* Users can override via `defaultArchitect` / `defaultCritic` in the service
|
|
1350
|
-
* config. Most installations should use the built-in defaults.
|
|
1351
|
-
*
|
|
1352
1348
|
* @module prompts
|
|
1353
1349
|
*/
|
|
1354
1350
|
const packageRoot = packageDirectorySync({
|
|
@@ -1836,7 +1832,7 @@ function buildArchitectTask(ctx, meta, config) {
|
|
|
1836
1832
|
const sections = [
|
|
1837
1833
|
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
1838
1834
|
'',
|
|
1839
|
-
|
|
1835
|
+
DEFAULT_ARCHITECT_PROMPT,
|
|
1840
1836
|
'',
|
|
1841
1837
|
'## SCOPE',
|
|
1842
1838
|
`Path: ${ctx.path}`,
|
|
@@ -1885,7 +1881,7 @@ function buildBuilderTask(ctx, meta, config) {
|
|
|
1885
1881
|
includeSteer: false,
|
|
1886
1882
|
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
1887
1883
|
});
|
|
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.');
|
|
1884
|
+
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
1885
|
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
1890
1886
|
}
|
|
1891
1887
|
/**
|
|
@@ -1900,7 +1896,7 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
1900
1896
|
const sections = [
|
|
1901
1897
|
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
1902
1898
|
'',
|
|
1903
|
-
|
|
1899
|
+
DEFAULT_CRITIC_PROMPT,
|
|
1904
1900
|
'',
|
|
1905
1901
|
'## SYNTHESIS TO EVALUATE',
|
|
1906
1902
|
meta._content ?? '(No content produced)',
|
|
@@ -2317,8 +2313,8 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
|
|
|
2317
2313
|
// through real builder cycles, architect runs with the current prompt and
|
|
2318
2314
|
// the snapshot updates. Coupling prompt changes to invalidation causes a
|
|
2319
2315
|
// corpus-wide synthesis storm (see #163).
|
|
2320
|
-
const architectChanged = isPromptStale(meta._architect,
|
|
2321
|
-
const criticChanged = isPromptStale(meta._critic,
|
|
2316
|
+
const architectChanged = isPromptStale(meta._architect, DEFAULT_ARCHITECT_PROMPT);
|
|
2317
|
+
const criticChanged = isPromptStale(meta._critic, DEFAULT_CRITIC_PROMPT);
|
|
2322
2318
|
const effectiveSynthesisCount = meta._synthesisCount ?? 0;
|
|
2323
2319
|
// _crossRefs declaration change
|
|
2324
2320
|
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
@@ -2565,10 +2561,11 @@ function parseArchitectOutput(output) {
|
|
|
2565
2561
|
/**
|
|
2566
2562
|
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
2567
2563
|
*
|
|
2568
|
-
* Attempts JSON
|
|
2564
|
+
* Attempts JSON extraction via multiple strategies. Returns null if all strategies fail
|
|
2565
|
+
* (e.g. the output is plain text, self-talk, or a sub-agent delegation response).
|
|
2569
2566
|
*
|
|
2570
2567
|
* @param output - Raw subprocess output.
|
|
2571
|
-
* @returns Parsed builder output
|
|
2568
|
+
* @returns Parsed builder output, or null if output is not valid builder JSON.
|
|
2572
2569
|
*/
|
|
2573
2570
|
function parseBuilderOutput(output) {
|
|
2574
2571
|
const trimmed = stripSentinel(output);
|
|
@@ -2597,8 +2594,8 @@ function parseBuilderOutput(output) {
|
|
|
2597
2594
|
if (result)
|
|
2598
2595
|
return result;
|
|
2599
2596
|
}
|
|
2600
|
-
//
|
|
2601
|
-
return
|
|
2597
|
+
// All JSON strategies failed — not valid builder output
|
|
2598
|
+
return null;
|
|
2602
2599
|
}
|
|
2603
2600
|
/** Try to parse a string as JSON and extract builder output fields. */
|
|
2604
2601
|
function tryParseJson(str) {
|
|
@@ -2726,7 +2723,7 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
2726
2723
|
ps = architectSuccess(ps);
|
|
2727
2724
|
const architectUpdates = {
|
|
2728
2725
|
_builder: builderBrief,
|
|
2729
|
-
_architect:
|
|
2726
|
+
_architect: DEFAULT_ARCHITECT_PROMPT,
|
|
2730
2727
|
_synthesisCount: 0,
|
|
2731
2728
|
_architectTokens: architectTokens,
|
|
2732
2729
|
_generatedAt: new Date().toISOString(),
|
|
@@ -2798,6 +2795,12 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
2798
2795
|
return { executed: true, phaseState: ps, updatedMeta };
|
|
2799
2796
|
}
|
|
2800
2797
|
const builderOutput = parseBuilderOutput(rawOutput);
|
|
2798
|
+
if (!builderOutput) {
|
|
2799
|
+
throw new Error('Builder output is not valid JSON — expected { _content|content: string, ... }. ' +
|
|
2800
|
+
'Raw output (' +
|
|
2801
|
+
rawOutput.length.toString() +
|
|
2802
|
+
' chars) did not match any JSON extraction strategy.');
|
|
2803
|
+
}
|
|
2801
2804
|
const builderTokens = result.tokens;
|
|
2802
2805
|
// Builder success: builder → fresh, critic → pending
|
|
2803
2806
|
ps = builderSuccess(ps);
|
|
@@ -2829,7 +2832,8 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
2829
2832
|
try {
|
|
2830
2833
|
const raw = await readFile(err.outputPath, 'utf8');
|
|
2831
2834
|
const partial = parseBuilderOutput(raw);
|
|
2832
|
-
if (partial
|
|
2835
|
+
if (partial !== null &&
|
|
2836
|
+
partial.state !== undefined &&
|
|
2833
2837
|
JSON.stringify(partial.state) !== JSON.stringify(currentMeta._state)) {
|
|
2834
2838
|
partialState = { _state: partial.state };
|
|
2835
2839
|
}
|
|
@@ -2872,7 +2876,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
2872
2876
|
const cycleComplete = isFullyFresh(ps);
|
|
2873
2877
|
const updates = {
|
|
2874
2878
|
_feedback: feedback,
|
|
2875
|
-
_critic:
|
|
2879
|
+
_critic: DEFAULT_CRITIC_PROMPT,
|
|
2876
2880
|
_criticTokens: criticTokens,
|
|
2877
2881
|
_error: undefined,
|
|
2878
2882
|
};
|
|
@@ -3067,95 +3071,118 @@ async function executePhase(node, currentMeta, phaseState, phase, config, execut
|
|
|
3067
3071
|
/**
|
|
3068
3072
|
* Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
|
|
3069
3073
|
*
|
|
3074
|
+
* Progress events are rendered using Handlebars templates. Owner-path links
|
|
3075
|
+
* are resolved via the jeeves-server resolve-path API, with graceful
|
|
3076
|
+
* fallback to raw filesystem paths when the server is unreachable.
|
|
3077
|
+
*
|
|
3070
3078
|
* @module progress
|
|
3071
3079
|
*/
|
|
3080
|
+
/** Compile raw template strings into Handlebars delegates. */
|
|
3081
|
+
function compileTemplates(raw) {
|
|
3082
|
+
const merged = {
|
|
3083
|
+
phaseStart: raw?.phaseStart ?? DEFAULT_TEMPLATE_STRINGS.phaseStart,
|
|
3084
|
+
phaseEnd: raw?.phaseEnd ?? DEFAULT_TEMPLATE_STRINGS.phaseEnd,
|
|
3085
|
+
phaseError: raw?.phaseError ?? DEFAULT_TEMPLATE_STRINGS.phaseError,
|
|
3086
|
+
};
|
|
3087
|
+
return {
|
|
3088
|
+
phaseStart: Handlebars.compile(merged.phaseStart),
|
|
3089
|
+
phaseEnd: Handlebars.compile(merged.phaseEnd),
|
|
3090
|
+
phaseError: Handlebars.compile(merged.phaseError),
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3072
3093
|
function formatNumber(n) {
|
|
3073
3094
|
return n.toLocaleString('en-US');
|
|
3074
3095
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
function
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
.
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
/** 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);
|
|
3101
|
-
if (!serverBaseUrl)
|
|
3102
|
-
return normalized;
|
|
3103
|
-
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
3104
|
-
return `${base}/path${encoded}`;
|
|
3105
|
-
}
|
|
3106
|
-
/** 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`;
|
|
3110
|
-
const encoded = encodePathSegments(metaJsonPath);
|
|
3111
|
-
if (!serverBaseUrl)
|
|
3112
|
-
return metaJsonPath;
|
|
3113
|
-
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
3114
|
-
return `${base}/path${encoded}`;
|
|
3115
|
-
}
|
|
3116
|
-
function formatProgressEvent(event, serverBaseUrl) {
|
|
3117
|
-
switch (event.type) {
|
|
3118
|
-
case 'synthesis_start': {
|
|
3119
|
-
const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
|
|
3120
|
-
return `🔬 Started meta synthesis: ${dirLink}`;
|
|
3121
|
-
}
|
|
3122
|
-
case 'phase_start': {
|
|
3123
|
-
if (!event.phase) {
|
|
3124
|
-
return ' ⚙️ Phase started';
|
|
3125
|
-
}
|
|
3126
|
-
return ` ⚙️ ${titleCasePhase(event.phase)} phase started`;
|
|
3096
|
+
/**
|
|
3097
|
+
* Resolve a filesystem path to a browse URL via the jeeves-server
|
|
3098
|
+
* resolve-path API. Returns the raw path as fallback on any failure.
|
|
3099
|
+
*
|
|
3100
|
+
* On HTTP 200, uses publicUrl if present, otherwise prefixes serverUrl
|
|
3101
|
+
* to browseUrl. On any error or non-200 response, returns fsPath as-is.
|
|
3102
|
+
*/
|
|
3103
|
+
/** Timeout for resolve-path API calls (ms). */
|
|
3104
|
+
const RESOLVE_PATH_TIMEOUT_MS = 3000;
|
|
3105
|
+
async function resolveLink(fsPath, serverUrl) {
|
|
3106
|
+
try {
|
|
3107
|
+
const url = new URL('/api/resolve-path', serverUrl);
|
|
3108
|
+
url.searchParams.set('fsPath', fsPath);
|
|
3109
|
+
const res = await fetchWithTimeout(url.toString(), RESOLVE_PATH_TIMEOUT_MS);
|
|
3110
|
+
if (!res.ok)
|
|
3111
|
+
return fsPath;
|
|
3112
|
+
const data = (await res.json());
|
|
3113
|
+
if (data.publicUrl)
|
|
3114
|
+
return data.publicUrl;
|
|
3115
|
+
if (data.browseUrl) {
|
|
3116
|
+
const base = serverUrl.replace(/\/+$/, '');
|
|
3117
|
+
return base + data.browseUrl;
|
|
3127
3118
|
}
|
|
3119
|
+
return fsPath;
|
|
3120
|
+
}
|
|
3121
|
+
catch {
|
|
3122
|
+
return fsPath;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
/** Resolve browse links for both the owner directory and its meta.json. */
|
|
3126
|
+
async function resolveLinks(ownerPath, serverUrl) {
|
|
3127
|
+
const dirLink = await resolveLink(ownerPath, serverUrl);
|
|
3128
|
+
// Derive metaLink by appending /.meta/meta.json to the resolved dir link
|
|
3129
|
+
const metaLink = dirLink.replace(/\/+$/, '') + '/.meta/meta.json';
|
|
3130
|
+
return { dirLink, metaLink };
|
|
3131
|
+
}
|
|
3132
|
+
/**
|
|
3133
|
+
* Render the appropriate template for a progress event.
|
|
3134
|
+
*
|
|
3135
|
+
* @param event - The progress event to render.
|
|
3136
|
+
* @param templates - Compiled Handlebars templates.
|
|
3137
|
+
* @param serverUrl - jeeves-server URL for resolve-path API.
|
|
3138
|
+
* @returns Rendered message string.
|
|
3139
|
+
*/
|
|
3140
|
+
async function renderProgressEvent(event, templates, serverUrl) {
|
|
3141
|
+
const ownerPath = event.path;
|
|
3142
|
+
const metaPath = ownerPath.replace(/\/+$/, '') + '/.meta/meta.json';
|
|
3143
|
+
const phase = (event.phase ?? 'unknown').toUpperCase();
|
|
3144
|
+
const { dirLink, metaLink } = await resolveLinks(ownerPath, serverUrl);
|
|
3145
|
+
const base = {
|
|
3146
|
+
dirPath: ownerPath,
|
|
3147
|
+
metaPath,
|
|
3148
|
+
dirLink,
|
|
3149
|
+
metaLink,
|
|
3150
|
+
phase,
|
|
3151
|
+
};
|
|
3152
|
+
switch (event.type) {
|
|
3153
|
+
case 'phase_start':
|
|
3154
|
+
return templates.phaseStart(base);
|
|
3128
3155
|
case 'phase_complete': {
|
|
3129
|
-
const
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
case 'synthesis_complete': {
|
|
3135
|
-
const metaLink = buildMetaJsonLink(event.path, serverBaseUrl);
|
|
3136
|
-
const tokenStr = formatTokens(event.tokens);
|
|
3137
|
-
const duration = event.durationMs !== undefined
|
|
3138
|
-
? formatSeconds(event.durationMs)
|
|
3139
|
-
: '0.0s';
|
|
3140
|
-
return `✅ Completed: ${metaLink} (${tokenStr} / ${duration})`;
|
|
3156
|
+
const seconds = event.durationMs !== undefined
|
|
3157
|
+
? String(Math.round(event.durationMs / 1000))
|
|
3158
|
+
: '0';
|
|
3159
|
+
const tokens = event.tokens !== undefined ? formatNumber(event.tokens) : 'unknown';
|
|
3160
|
+
return templates.phaseEnd({ ...base, seconds, tokens });
|
|
3141
3161
|
}
|
|
3142
3162
|
case 'error': {
|
|
3143
|
-
const
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3163
|
+
const seconds = event.durationMs !== undefined
|
|
3164
|
+
? String(Math.round(event.durationMs / 1000))
|
|
3165
|
+
: '0';
|
|
3166
|
+
const errorMsg = event.error ?? 'Unknown error';
|
|
3167
|
+
return templates.phaseError({ ...base, seconds, error: errorMsg });
|
|
3147
3168
|
}
|
|
3148
|
-
default:
|
|
3169
|
+
default:
|
|
3149
3170
|
return 'Unknown progress event';
|
|
3150
|
-
}
|
|
3151
3171
|
}
|
|
3152
3172
|
}
|
|
3153
3173
|
class ProgressReporter {
|
|
3154
3174
|
config;
|
|
3155
3175
|
logger;
|
|
3176
|
+
templates;
|
|
3177
|
+
/** Snapshot of template source strings used to compile `this.templates`. */
|
|
3178
|
+
lastTemplateSource;
|
|
3179
|
+
serverUrl;
|
|
3156
3180
|
constructor(config, logger) {
|
|
3157
3181
|
this.config = config;
|
|
3158
3182
|
this.logger = logger;
|
|
3183
|
+
this.templates = compileTemplates(config.templates);
|
|
3184
|
+
this.lastTemplateSource = JSON.stringify(config.templates);
|
|
3185
|
+
this.serverUrl = config.serverUrl ?? 'http://127.0.0.1:1934';
|
|
3159
3186
|
}
|
|
3160
3187
|
async report(event) {
|
|
3161
3188
|
// Multi-channel mode: reportTarget is the destination, reportChannel is the platform.
|
|
@@ -3163,7 +3190,27 @@ class ProgressReporter {
|
|
|
3163
3190
|
const target = this.config.reportTarget ?? this.config.reportChannel;
|
|
3164
3191
|
if (!target)
|
|
3165
3192
|
return;
|
|
3166
|
-
|
|
3193
|
+
// Detect template hot-reload: config.templates may be mutated by
|
|
3194
|
+
// applyHotReloadedConfig; recompile when the source strings change.
|
|
3195
|
+
const currentSource = JSON.stringify(this.config.templates);
|
|
3196
|
+
if (currentSource !== this.lastTemplateSource) {
|
|
3197
|
+
try {
|
|
3198
|
+
this.templates = compileTemplates(this.config.templates);
|
|
3199
|
+
this.lastTemplateSource = currentSource;
|
|
3200
|
+
this.logger.info('Progress templates recompiled (hot-reload)');
|
|
3201
|
+
}
|
|
3202
|
+
catch (err) {
|
|
3203
|
+
this.logger.warn({ err }, 'Failed to compile hot-reloaded templates; keeping previous templates');
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
let message;
|
|
3207
|
+
try {
|
|
3208
|
+
message = await renderProgressEvent(event, this.templates, this.serverUrl);
|
|
3209
|
+
}
|
|
3210
|
+
catch (err) {
|
|
3211
|
+
this.logger.warn({ err }, 'Progress event rendering failed');
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3167
3214
|
const url = new URL('/tools/invoke', this.config.gatewayUrl);
|
|
3168
3215
|
const args = {
|
|
3169
3216
|
action: 'send',
|
|
@@ -4741,6 +4788,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4741
4788
|
critic: emptyPhaseCounts(),
|
|
4742
4789
|
};
|
|
4743
4790
|
let nextPhase = null;
|
|
4791
|
+
let metaCounts = null;
|
|
4744
4792
|
try {
|
|
4745
4793
|
const metaResult = await cache.get(config, watcher);
|
|
4746
4794
|
// Count raw phase states (before retry) for display
|
|
@@ -4762,6 +4810,17 @@ function registerStatusRoute(app, deps) {
|
|
|
4762
4810
|
staleness: winner.effectiveStaleness,
|
|
4763
4811
|
};
|
|
4764
4812
|
}
|
|
4813
|
+
// Meta counts summary
|
|
4814
|
+
const summary = computeSummary(metaResult.entries, config.depthWeight);
|
|
4815
|
+
metaCounts = {
|
|
4816
|
+
total: summary.total,
|
|
4817
|
+
enabled: summary.total - summary.disabled,
|
|
4818
|
+
disabled: summary.disabled,
|
|
4819
|
+
neverSynthesized: summary.neverSynthesized,
|
|
4820
|
+
stale: summary.stale,
|
|
4821
|
+
errors: summary.errors,
|
|
4822
|
+
locked: summary.locked,
|
|
4823
|
+
};
|
|
4765
4824
|
}
|
|
4766
4825
|
catch {
|
|
4767
4826
|
// Watcher unreachable — phase summary unavailable
|
|
@@ -4790,6 +4849,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4790
4849
|
},
|
|
4791
4850
|
phaseStateSummary,
|
|
4792
4851
|
nextPhase,
|
|
4852
|
+
metaCounts,
|
|
4793
4853
|
};
|
|
4794
4854
|
},
|
|
4795
4855
|
});
|
|
@@ -5099,11 +5159,10 @@ class HttpWatcherClient {
|
|
|
5099
5159
|
async post(endpoint, body) {
|
|
5100
5160
|
const url = this.baseUrl + endpoint;
|
|
5101
5161
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
5102
|
-
const res = await
|
|
5162
|
+
const res = await fetchWithTimeout(url, this.timeoutMs, {
|
|
5103
5163
|
method: 'POST',
|
|
5104
5164
|
headers: { 'Content-Type': 'application/json' },
|
|
5105
5165
|
body: JSON.stringify(body),
|
|
5106
|
-
signal: AbortSignal.timeout(this.timeoutMs),
|
|
5107
5166
|
});
|
|
5108
5167
|
if (res.ok) {
|
|
5109
5168
|
return res.json();
|
|
@@ -5145,25 +5204,6 @@ class HttpWatcherClient {
|
|
|
5145
5204
|
*
|
|
5146
5205
|
* @module bootstrap
|
|
5147
5206
|
*/
|
|
5148
|
-
/**
|
|
5149
|
-
* Compute per-cycle token total from a completed meta.
|
|
5150
|
-
*
|
|
5151
|
-
* Exported for testing.
|
|
5152
|
-
*
|
|
5153
|
-
* Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
|
|
5154
|
-
* a value of 1 means architect ran this cycle (was 0 pre-increment),
|
|
5155
|
-
* so all three phase token fields are summed. A value \> 1 means architect
|
|
5156
|
-
* was skipped (cached brief reused), so only builder + critic are summed.
|
|
5157
|
-
*/
|
|
5158
|
-
function computeCycleTokens(meta) {
|
|
5159
|
-
const builderTokens = meta._builderTokens ?? 0;
|
|
5160
|
-
const criticTokens = meta._criticTokens ?? 0;
|
|
5161
|
-
const architectRan = (meta._synthesisCount ?? 1) === 1;
|
|
5162
|
-
const architectTokens = architectRan
|
|
5163
|
-
? (meta._architectTokens ?? 0)
|
|
5164
|
-
: 0;
|
|
5165
|
-
return architectTokens + builderTokens + criticTokens;
|
|
5166
|
-
}
|
|
5167
5207
|
/**
|
|
5168
5208
|
* Bootstrap the service: create logger, build server, start listening,
|
|
5169
5209
|
* wire scheduler, queue processing, rule registration, config hot-reload,
|
|
@@ -5240,16 +5280,7 @@ async function startService(config, configPath) {
|
|
|
5240
5280
|
// Track whether synthesis_start has been emitted (deferred until
|
|
5241
5281
|
// a phase actually begins, avoiding orphan "Started" messages
|
|
5242
5282
|
// when the meta is skipped/locked/fresh). See #165.
|
|
5243
|
-
let synthesisStartEmitted = false;
|
|
5244
5283
|
const result = await orchestratePhase(config, executor, watcher, path, async (evt) => {
|
|
5245
|
-
// Emit synthesis_start on first phase_start (deferred guard)
|
|
5246
|
-
if (evt.type === 'phase_start' && !synthesisStartEmitted) {
|
|
5247
|
-
synthesisStartEmitted = true;
|
|
5248
|
-
await progress.report({
|
|
5249
|
-
type: 'synthesis_start',
|
|
5250
|
-
path: ownerPath,
|
|
5251
|
-
});
|
|
5252
|
-
}
|
|
5253
5284
|
// Wire current-phase tracking for GET /queue and POST /synthesize/abort
|
|
5254
5285
|
if (evt.type === 'phase_start' && evt.phase) {
|
|
5255
5286
|
queue.setCurrentPhase(ownerPath, evt.phase);
|
|
@@ -5291,19 +5322,8 @@ async function startService(config, configPath) {
|
|
|
5291
5322
|
// Task #9: Reset backoff on ANY successful phase execution
|
|
5292
5323
|
scheduler.resetBackoff();
|
|
5293
5324
|
}
|
|
5294
|
-
//
|
|
5295
|
-
|
|
5296
|
-
const updatedMeta = result.phaseResult?.updatedMeta;
|
|
5297
|
-
const tokens = updatedMeta
|
|
5298
|
-
? computeCycleTokens(updatedMeta)
|
|
5299
|
-
: undefined;
|
|
5300
|
-
await progress.report({
|
|
5301
|
-
type: 'synthesis_complete',
|
|
5302
|
-
path: ownerPath,
|
|
5303
|
-
durationMs,
|
|
5304
|
-
tokens,
|
|
5305
|
-
});
|
|
5306
|
-
}
|
|
5325
|
+
// Note: synthesis_complete is no longer emitted; the phaseEnd template
|
|
5326
|
+
// for the final critic phase serves as the completion signal (#129).
|
|
5307
5327
|
}
|
|
5308
5328
|
catch (err) {
|
|
5309
5329
|
stats.totalErrors++;
|
|
@@ -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.
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type { InferenceRuleSpec, MetaContext, MetaExecutor, MetaSpawnOptions, Me
|
|
|
19
19
|
export type { LoggerConfig } from './logger/index.js';
|
|
20
20
|
export { createLogger, type MinimalLogger } from './logger/index.js';
|
|
21
21
|
export { buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, type BuilderOutput, orchestratePhase, type OrchestratePhaseResult, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, type PhaseProgressCallback, type PhaseResult, runArchitect, runBuilder, runCritic, } from './orchestrator/index.js';
|
|
22
|
-
export {
|
|
22
|
+
export { type ProgressEvent, type ProgressPhase, ProgressReporter, type ProgressReporterConfig, renderProgressEvent, type TemplateStrings, } from './progress/index.js';
|
|
23
23
|
export { actualStaleness, computeEffectiveStaleness, hasSteerChanged, isArchitectTriggered, isStale, MAX_STALENESS_SECONDS, type StalenessCandidate, } from './scheduling/index.js';
|
|
24
24
|
export { type MetaConfig, metaConfigSchema, type MetaError, metaErrorSchema, type MetaJson, metaJsonSchema, type ServiceConfig, serviceConfigSchema, } from './schema/index.js';
|
|
25
25
|
export { Scheduler } from './scheduler/index.js';
|