@karmaniverous/jeeves-meta 0.16.3 → 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/dist/cli/jeeves-meta/index.js +151 -152
- package/dist/index.d.ts +1 -1
- package/dist/index.js +152 -153
- package/dist/progress/index.d.ts +37 -8
- package/dist/schema/config.d.ts +12 -2
- package/dist/schema/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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';
|
|
@@ -763,6 +763,12 @@ function applyHotReloadedConfig(newConfig) {
|
|
|
763
763
|
*
|
|
764
764
|
* @module schema/config
|
|
765
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
|
+
};
|
|
766
772
|
/** Zod schema for logging configuration. */
|
|
767
773
|
const loggingSchema = z.object({
|
|
768
774
|
/** Log level. */
|
|
@@ -791,14 +797,24 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
791
797
|
reportChannel: z.string().optional(),
|
|
792
798
|
/** Channel/user ID to send progress messages to. */
|
|
793
799
|
reportTarget: z.string().optional(),
|
|
794
|
-
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
795
|
-
serverBaseUrl: z.string().optional(),
|
|
796
800
|
/**
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
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).
|
|
800
810
|
*/
|
|
801
|
-
|
|
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 })),
|
|
802
818
|
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
803
819
|
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
804
820
|
/** Logging configuration. */
|
|
@@ -3055,128 +3071,118 @@ async function executePhase(node, currentMeta, phaseState, phase, config, execut
|
|
|
3055
3071
|
/**
|
|
3056
3072
|
* Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
|
|
3057
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
|
+
*
|
|
3058
3078
|
* @module progress
|
|
3059
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
|
+
}
|
|
3060
3093
|
function formatNumber(n) {
|
|
3061
3094
|
return n.toLocaleString('en-US');
|
|
3062
3095
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
function
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
.
|
|
3083
|
-
|
|
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
|
-
}
|
|
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;
|
|
3113
3118
|
}
|
|
3119
|
+
return fsPath;
|
|
3114
3120
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
const
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
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
|
+
};
|
|
3138
3152
|
switch (event.type) {
|
|
3139
|
-
case '
|
|
3140
|
-
|
|
3141
|
-
return `🔬 Started meta synthesis: ${dirLink}`;
|
|
3142
|
-
}
|
|
3143
|
-
case 'phase_start': {
|
|
3144
|
-
if (!event.phase) {
|
|
3145
|
-
return ' ⚙️ Phase started';
|
|
3146
|
-
}
|
|
3147
|
-
return ` ⚙️ ${titleCasePhase(event.phase)} phase started`;
|
|
3148
|
-
}
|
|
3153
|
+
case 'phase_start':
|
|
3154
|
+
return templates.phaseStart(base);
|
|
3149
3155
|
case 'phase_complete': {
|
|
3150
|
-
const
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
case 'synthesis_complete': {
|
|
3156
|
-
const metaLink = buildMetaJsonLink(event.path, serverBaseUrl, serverDriveRoots);
|
|
3157
|
-
const tokenStr = formatTokens(event.tokens);
|
|
3158
|
-
const duration = event.durationMs !== undefined
|
|
3159
|
-
? formatSeconds(event.durationMs)
|
|
3160
|
-
: '0.0s';
|
|
3161
|
-
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 });
|
|
3162
3161
|
}
|
|
3163
3162
|
case 'error': {
|
|
3164
|
-
const
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
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 });
|
|
3168
3168
|
}
|
|
3169
|
-
default:
|
|
3169
|
+
default:
|
|
3170
3170
|
return 'Unknown progress event';
|
|
3171
|
-
}
|
|
3172
3171
|
}
|
|
3173
3172
|
}
|
|
3174
3173
|
class ProgressReporter {
|
|
3175
3174
|
config;
|
|
3176
3175
|
logger;
|
|
3176
|
+
templates;
|
|
3177
|
+
/** Snapshot of template source strings used to compile `this.templates`. */
|
|
3178
|
+
lastTemplateSource;
|
|
3179
|
+
serverUrl;
|
|
3177
3180
|
constructor(config, logger) {
|
|
3178
3181
|
this.config = config;
|
|
3179
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';
|
|
3180
3186
|
}
|
|
3181
3187
|
async report(event) {
|
|
3182
3188
|
// Multi-channel mode: reportTarget is the destination, reportChannel is the platform.
|
|
@@ -3184,7 +3190,27 @@ class ProgressReporter {
|
|
|
3184
3190
|
const target = this.config.reportTarget ?? this.config.reportChannel;
|
|
3185
3191
|
if (!target)
|
|
3186
3192
|
return;
|
|
3187
|
-
|
|
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
|
+
}
|
|
3188
3214
|
const url = new URL('/tools/invoke', this.config.gatewayUrl);
|
|
3189
3215
|
const args = {
|
|
3190
3216
|
action: 'send',
|
|
@@ -4762,6 +4788,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4762
4788
|
critic: emptyPhaseCounts(),
|
|
4763
4789
|
};
|
|
4764
4790
|
let nextPhase = null;
|
|
4791
|
+
let metaCounts = null;
|
|
4765
4792
|
try {
|
|
4766
4793
|
const metaResult = await cache.get(config, watcher);
|
|
4767
4794
|
// Count raw phase states (before retry) for display
|
|
@@ -4783,6 +4810,17 @@ function registerStatusRoute(app, deps) {
|
|
|
4783
4810
|
staleness: winner.effectiveStaleness,
|
|
4784
4811
|
};
|
|
4785
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
|
+
};
|
|
4786
4824
|
}
|
|
4787
4825
|
catch {
|
|
4788
4826
|
// Watcher unreachable — phase summary unavailable
|
|
@@ -4811,6 +4849,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4811
4849
|
},
|
|
4812
4850
|
phaseStateSummary,
|
|
4813
4851
|
nextPhase,
|
|
4852
|
+
metaCounts,
|
|
4814
4853
|
};
|
|
4815
4854
|
},
|
|
4816
4855
|
});
|
|
@@ -5120,11 +5159,10 @@ class HttpWatcherClient {
|
|
|
5120
5159
|
async post(endpoint, body) {
|
|
5121
5160
|
const url = this.baseUrl + endpoint;
|
|
5122
5161
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
5123
|
-
const res = await
|
|
5162
|
+
const res = await fetchWithTimeout(url, this.timeoutMs, {
|
|
5124
5163
|
method: 'POST',
|
|
5125
5164
|
headers: { 'Content-Type': 'application/json' },
|
|
5126
5165
|
body: JSON.stringify(body),
|
|
5127
|
-
signal: AbortSignal.timeout(this.timeoutMs),
|
|
5128
5166
|
});
|
|
5129
5167
|
if (res.ok) {
|
|
5130
5168
|
return res.json();
|
|
@@ -5166,25 +5204,6 @@ class HttpWatcherClient {
|
|
|
5166
5204
|
*
|
|
5167
5205
|
* @module bootstrap
|
|
5168
5206
|
*/
|
|
5169
|
-
/**
|
|
5170
|
-
* Compute per-cycle token total from a completed meta.
|
|
5171
|
-
*
|
|
5172
|
-
* Exported for testing.
|
|
5173
|
-
*
|
|
5174
|
-
* Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
|
|
5175
|
-
* a value of 1 means architect ran this cycle (was 0 pre-increment),
|
|
5176
|
-
* so all three phase token fields are summed. A value \> 1 means architect
|
|
5177
|
-
* was skipped (cached brief reused), so only builder + critic are summed.
|
|
5178
|
-
*/
|
|
5179
|
-
function computeCycleTokens(meta) {
|
|
5180
|
-
const builderTokens = meta._builderTokens ?? 0;
|
|
5181
|
-
const criticTokens = meta._criticTokens ?? 0;
|
|
5182
|
-
const architectRan = (meta._synthesisCount ?? 1) === 1;
|
|
5183
|
-
const architectTokens = architectRan
|
|
5184
|
-
? (meta._architectTokens ?? 0)
|
|
5185
|
-
: 0;
|
|
5186
|
-
return architectTokens + builderTokens + criticTokens;
|
|
5187
|
-
}
|
|
5188
5207
|
/**
|
|
5189
5208
|
* Bootstrap the service: create logger, build server, start listening,
|
|
5190
5209
|
* wire scheduler, queue processing, rule registration, config hot-reload,
|
|
@@ -5261,16 +5280,7 @@ async function startService(config, configPath) {
|
|
|
5261
5280
|
// Track whether synthesis_start has been emitted (deferred until
|
|
5262
5281
|
// a phase actually begins, avoiding orphan "Started" messages
|
|
5263
5282
|
// when the meta is skipped/locked/fresh). See #165.
|
|
5264
|
-
let synthesisStartEmitted = false;
|
|
5265
5283
|
const result = await orchestratePhase(config, executor, watcher, path, async (evt) => {
|
|
5266
|
-
// Emit synthesis_start on first phase_start (deferred guard)
|
|
5267
|
-
if (evt.type === 'phase_start' && !synthesisStartEmitted) {
|
|
5268
|
-
synthesisStartEmitted = true;
|
|
5269
|
-
await progress.report({
|
|
5270
|
-
type: 'synthesis_start',
|
|
5271
|
-
path: ownerPath,
|
|
5272
|
-
});
|
|
5273
|
-
}
|
|
5274
5284
|
// Wire current-phase tracking for GET /queue and POST /synthesize/abort
|
|
5275
5285
|
if (evt.type === 'phase_start' && evt.phase) {
|
|
5276
5286
|
queue.setCurrentPhase(ownerPath, evt.phase);
|
|
@@ -5312,19 +5322,8 @@ async function startService(config, configPath) {
|
|
|
5312
5322
|
// Task #9: Reset backoff on ANY successful phase execution
|
|
5313
5323
|
scheduler.resetBackoff();
|
|
5314
5324
|
}
|
|
5315
|
-
//
|
|
5316
|
-
|
|
5317
|
-
const updatedMeta = result.phaseResult?.updatedMeta;
|
|
5318
|
-
const tokens = updatedMeta
|
|
5319
|
-
? computeCycleTokens(updatedMeta)
|
|
5320
|
-
: undefined;
|
|
5321
|
-
await progress.report({
|
|
5322
|
-
type: 'synthesis_complete',
|
|
5323
|
-
path: ownerPath,
|
|
5324
|
-
durationMs,
|
|
5325
|
-
tokens,
|
|
5326
|
-
});
|
|
5327
|
-
}
|
|
5325
|
+
// Note: synthesis_complete is no longer emitted; the phaseEnd template
|
|
5326
|
+
// for the final critic phase serves as the completion signal (#129).
|
|
5328
5327
|
}
|
|
5329
5328
|
catch (err) {
|
|
5330
5329
|
stats.totalErrors++;
|
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';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { normalizePath, META_COMPONENT, metaConfigSchema, getEndpoint, phaseStat
|
|
|
5
5
|
export { metaConfigSchema, metaErrorSchema, normalizePath } from '@karmaniverous/jeeves-meta-core';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import process$1 from 'node:process';
|
|
8
|
-
import { fetchJson, postJson, getServiceUrl, sleepAsync, createConfigQueryHandler, atomicWrite, createStatusHandler, getBindAddress, jeevesComponentDescriptorSchema } from '@karmaniverous/jeeves';
|
|
8
|
+
import { fetchJson, postJson, getServiceUrl, sleepAsync, fetchWithTimeout, createConfigQueryHandler, atomicWrite, createStatusHandler, getBindAddress, jeevesComponentDescriptorSchema } from '@karmaniverous/jeeves';
|
|
9
9
|
export { sleepAsync as sleep } from '@karmaniverous/jeeves';
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { randomUUID, createHash } from 'node:crypto';
|
|
@@ -1252,6 +1252,12 @@ function applyHotReloadedConfig(newConfig) {
|
|
|
1252
1252
|
*
|
|
1253
1253
|
* @module schema/config
|
|
1254
1254
|
*/
|
|
1255
|
+
/** Default Handlebars template strings for progress reporting. Single source of truth. */
|
|
1256
|
+
const DEFAULT_TEMPLATE_STRINGS = {
|
|
1257
|
+
phaseStart: ':gear: Started meta synthesis {{phase}} phase of <{{dirLink}}>',
|
|
1258
|
+
phaseEnd: ':white_check_mark: Completed meta synthesis {{phase}} phase ({{tokens}} tokens / {{seconds}}s) at <{{metaLink}}>',
|
|
1259
|
+
phaseError: ':x: Meta synthesis {{phase}} phase failed at <{{dirLink}}>\n Error: {{error}}',
|
|
1260
|
+
};
|
|
1255
1261
|
/** Zod schema for logging configuration. */
|
|
1256
1262
|
const loggingSchema = z.object({
|
|
1257
1263
|
/** Log level. */
|
|
@@ -1280,14 +1286,24 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
1280
1286
|
reportChannel: z.string().optional(),
|
|
1281
1287
|
/** Channel/user ID to send progress messages to. */
|
|
1282
1288
|
reportTarget: z.string().optional(),
|
|
1283
|
-
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
1284
|
-
serverBaseUrl: z.string().optional(),
|
|
1285
1289
|
/**
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1290
|
+
* URL of the local jeeves-server instance, used by the ProgressReporter
|
|
1291
|
+
* to resolve filesystem paths to browse links via the resolve-path API.
|
|
1292
|
+
* Default: http://127.0.0.1:1934
|
|
1293
|
+
*/
|
|
1294
|
+
serverUrl: z.string().default('http://127.0.0.1:1934'),
|
|
1295
|
+
/**
|
|
1296
|
+
* Handlebars templates for progress reporting messages.
|
|
1297
|
+
* Each template receives a standard set of data keys (dirLink, metaLink,
|
|
1298
|
+
* phase, tokens, seconds, error).
|
|
1289
1299
|
*/
|
|
1290
|
-
|
|
1300
|
+
templates: z
|
|
1301
|
+
.object({
|
|
1302
|
+
phaseStart: z.string().default(DEFAULT_TEMPLATE_STRINGS.phaseStart),
|
|
1303
|
+
phaseEnd: z.string().default(DEFAULT_TEMPLATE_STRINGS.phaseEnd),
|
|
1304
|
+
phaseError: z.string().default(DEFAULT_TEMPLATE_STRINGS.phaseError),
|
|
1305
|
+
})
|
|
1306
|
+
.default(() => ({ ...DEFAULT_TEMPLATE_STRINGS })),
|
|
1291
1307
|
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
1292
1308
|
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
1293
1309
|
/** Logging configuration. */
|
|
@@ -3313,128 +3329,118 @@ async function executePhase(node, currentMeta, phaseState, phase, config, execut
|
|
|
3313
3329
|
/**
|
|
3314
3330
|
* Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
|
|
3315
3331
|
*
|
|
3332
|
+
* Progress events are rendered using Handlebars templates. Owner-path links
|
|
3333
|
+
* are resolved via the jeeves-server resolve-path API, with graceful
|
|
3334
|
+
* fallback to raw filesystem paths when the server is unreachable.
|
|
3335
|
+
*
|
|
3316
3336
|
* @module progress
|
|
3317
3337
|
*/
|
|
3338
|
+
/** Compile raw template strings into Handlebars delegates. */
|
|
3339
|
+
function compileTemplates(raw) {
|
|
3340
|
+
const merged = {
|
|
3341
|
+
phaseStart: raw?.phaseStart ?? DEFAULT_TEMPLATE_STRINGS.phaseStart,
|
|
3342
|
+
phaseEnd: raw?.phaseEnd ?? DEFAULT_TEMPLATE_STRINGS.phaseEnd,
|
|
3343
|
+
phaseError: raw?.phaseError ?? DEFAULT_TEMPLATE_STRINGS.phaseError,
|
|
3344
|
+
};
|
|
3345
|
+
return {
|
|
3346
|
+
phaseStart: Handlebars.compile(merged.phaseStart),
|
|
3347
|
+
phaseEnd: Handlebars.compile(merged.phaseEnd),
|
|
3348
|
+
phaseError: Handlebars.compile(merged.phaseError),
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3318
3351
|
function formatNumber(n) {
|
|
3319
3352
|
return n.toLocaleString('en-US');
|
|
3320
3353
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
function
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
.
|
|
3341
|
-
|
|
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
|
-
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Resolve a filesystem path to a browse URL via the jeeves-server
|
|
3356
|
+
* resolve-path API. Returns the raw path as fallback on any failure.
|
|
3357
|
+
*
|
|
3358
|
+
* On HTTP 200, uses publicUrl if present, otherwise prefixes serverUrl
|
|
3359
|
+
* to browseUrl. On any error or non-200 response, returns fsPath as-is.
|
|
3360
|
+
*/
|
|
3361
|
+
/** Timeout for resolve-path API calls (ms). */
|
|
3362
|
+
const RESOLVE_PATH_TIMEOUT_MS = 3000;
|
|
3363
|
+
async function resolveLink(fsPath, serverUrl) {
|
|
3364
|
+
try {
|
|
3365
|
+
const url = new URL('/api/resolve-path', serverUrl);
|
|
3366
|
+
url.searchParams.set('fsPath', fsPath);
|
|
3367
|
+
const res = await fetchWithTimeout(url.toString(), RESOLVE_PATH_TIMEOUT_MS);
|
|
3368
|
+
if (!res.ok)
|
|
3369
|
+
return fsPath;
|
|
3370
|
+
const data = (await res.json());
|
|
3371
|
+
if (data.publicUrl)
|
|
3372
|
+
return data.publicUrl;
|
|
3373
|
+
if (data.browseUrl) {
|
|
3374
|
+
const base = serverUrl.replace(/\/+$/, '');
|
|
3375
|
+
return base + data.browseUrl;
|
|
3371
3376
|
}
|
|
3377
|
+
return fsPath;
|
|
3372
3378
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
}
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
const
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3379
|
+
catch {
|
|
3380
|
+
return fsPath;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
/** Resolve browse links for both the owner directory and its meta.json. */
|
|
3384
|
+
async function resolveLinks(ownerPath, serverUrl) {
|
|
3385
|
+
const dirLink = await resolveLink(ownerPath, serverUrl);
|
|
3386
|
+
// Derive metaLink by appending /.meta/meta.json to the resolved dir link
|
|
3387
|
+
const metaLink = dirLink.replace(/\/+$/, '') + '/.meta/meta.json';
|
|
3388
|
+
return { dirLink, metaLink };
|
|
3389
|
+
}
|
|
3390
|
+
/**
|
|
3391
|
+
* Render the appropriate template for a progress event.
|
|
3392
|
+
*
|
|
3393
|
+
* @param event - The progress event to render.
|
|
3394
|
+
* @param templates - Compiled Handlebars templates.
|
|
3395
|
+
* @param serverUrl - jeeves-server URL for resolve-path API.
|
|
3396
|
+
* @returns Rendered message string.
|
|
3397
|
+
*/
|
|
3398
|
+
async function renderProgressEvent(event, templates, serverUrl) {
|
|
3399
|
+
const ownerPath = event.path;
|
|
3400
|
+
const metaPath = ownerPath.replace(/\/+$/, '') + '/.meta/meta.json';
|
|
3401
|
+
const phase = (event.phase ?? 'unknown').toUpperCase();
|
|
3402
|
+
const { dirLink, metaLink } = await resolveLinks(ownerPath, serverUrl);
|
|
3403
|
+
const base = {
|
|
3404
|
+
dirPath: ownerPath,
|
|
3405
|
+
metaPath,
|
|
3406
|
+
dirLink,
|
|
3407
|
+
metaLink,
|
|
3408
|
+
phase,
|
|
3409
|
+
};
|
|
3396
3410
|
switch (event.type) {
|
|
3397
|
-
case '
|
|
3398
|
-
|
|
3399
|
-
return `🔬 Started meta synthesis: ${dirLink}`;
|
|
3400
|
-
}
|
|
3401
|
-
case 'phase_start': {
|
|
3402
|
-
if (!event.phase) {
|
|
3403
|
-
return ' ⚙️ Phase started';
|
|
3404
|
-
}
|
|
3405
|
-
return ` ⚙️ ${titleCasePhase(event.phase)} phase started`;
|
|
3406
|
-
}
|
|
3411
|
+
case 'phase_start':
|
|
3412
|
+
return templates.phaseStart(base);
|
|
3407
3413
|
case 'phase_complete': {
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
case 'synthesis_complete': {
|
|
3414
|
-
const metaLink = buildMetaJsonLink(event.path, serverBaseUrl, serverDriveRoots);
|
|
3415
|
-
const tokenStr = formatTokens(event.tokens);
|
|
3416
|
-
const duration = event.durationMs !== undefined
|
|
3417
|
-
? formatSeconds(event.durationMs)
|
|
3418
|
-
: '0.0s';
|
|
3419
|
-
return `✅ Completed: ${metaLink} (${tokenStr} / ${duration})`;
|
|
3414
|
+
const seconds = event.durationMs !== undefined
|
|
3415
|
+
? String(Math.round(event.durationMs / 1000))
|
|
3416
|
+
: '0';
|
|
3417
|
+
const tokens = event.tokens !== undefined ? formatNumber(event.tokens) : 'unknown';
|
|
3418
|
+
return templates.phaseEnd({ ...base, seconds, tokens });
|
|
3420
3419
|
}
|
|
3421
3420
|
case 'error': {
|
|
3422
|
-
const
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3421
|
+
const seconds = event.durationMs !== undefined
|
|
3422
|
+
? String(Math.round(event.durationMs / 1000))
|
|
3423
|
+
: '0';
|
|
3424
|
+
const errorMsg = event.error ?? 'Unknown error';
|
|
3425
|
+
return templates.phaseError({ ...base, seconds, error: errorMsg });
|
|
3426
3426
|
}
|
|
3427
|
-
default:
|
|
3427
|
+
default:
|
|
3428
3428
|
return 'Unknown progress event';
|
|
3429
|
-
}
|
|
3430
3429
|
}
|
|
3431
3430
|
}
|
|
3432
3431
|
class ProgressReporter {
|
|
3433
3432
|
config;
|
|
3434
3433
|
logger;
|
|
3434
|
+
templates;
|
|
3435
|
+
/** Snapshot of template source strings used to compile `this.templates`. */
|
|
3436
|
+
lastTemplateSource;
|
|
3437
|
+
serverUrl;
|
|
3435
3438
|
constructor(config, logger) {
|
|
3436
3439
|
this.config = config;
|
|
3437
3440
|
this.logger = logger;
|
|
3441
|
+
this.templates = compileTemplates(config.templates);
|
|
3442
|
+
this.lastTemplateSource = JSON.stringify(config.templates);
|
|
3443
|
+
this.serverUrl = config.serverUrl ?? 'http://127.0.0.1:1934';
|
|
3438
3444
|
}
|
|
3439
3445
|
async report(event) {
|
|
3440
3446
|
// Multi-channel mode: reportTarget is the destination, reportChannel is the platform.
|
|
@@ -3442,7 +3448,27 @@ class ProgressReporter {
|
|
|
3442
3448
|
const target = this.config.reportTarget ?? this.config.reportChannel;
|
|
3443
3449
|
if (!target)
|
|
3444
3450
|
return;
|
|
3445
|
-
|
|
3451
|
+
// Detect template hot-reload: config.templates may be mutated by
|
|
3452
|
+
// applyHotReloadedConfig; recompile when the source strings change.
|
|
3453
|
+
const currentSource = JSON.stringify(this.config.templates);
|
|
3454
|
+
if (currentSource !== this.lastTemplateSource) {
|
|
3455
|
+
try {
|
|
3456
|
+
this.templates = compileTemplates(this.config.templates);
|
|
3457
|
+
this.lastTemplateSource = currentSource;
|
|
3458
|
+
this.logger.info('Progress templates recompiled (hot-reload)');
|
|
3459
|
+
}
|
|
3460
|
+
catch (err) {
|
|
3461
|
+
this.logger.warn({ err }, 'Failed to compile hot-reloaded templates; keeping previous templates');
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
let message;
|
|
3465
|
+
try {
|
|
3466
|
+
message = await renderProgressEvent(event, this.templates, this.serverUrl);
|
|
3467
|
+
}
|
|
3468
|
+
catch (err) {
|
|
3469
|
+
this.logger.warn({ err }, 'Progress event rendering failed');
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3446
3472
|
const url = new URL('/tools/invoke', this.config.gatewayUrl);
|
|
3447
3473
|
const args = {
|
|
3448
3474
|
action: 'send',
|
|
@@ -4992,6 +5018,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4992
5018
|
critic: emptyPhaseCounts(),
|
|
4993
5019
|
};
|
|
4994
5020
|
let nextPhase = null;
|
|
5021
|
+
let metaCounts = null;
|
|
4995
5022
|
try {
|
|
4996
5023
|
const metaResult = await cache.get(config, watcher);
|
|
4997
5024
|
// Count raw phase states (before retry) for display
|
|
@@ -5013,6 +5040,17 @@ function registerStatusRoute(app, deps) {
|
|
|
5013
5040
|
staleness: winner.effectiveStaleness,
|
|
5014
5041
|
};
|
|
5015
5042
|
}
|
|
5043
|
+
// Meta counts summary
|
|
5044
|
+
const summary = computeSummary(metaResult.entries, config.depthWeight);
|
|
5045
|
+
metaCounts = {
|
|
5046
|
+
total: summary.total,
|
|
5047
|
+
enabled: summary.total - summary.disabled,
|
|
5048
|
+
disabled: summary.disabled,
|
|
5049
|
+
neverSynthesized: summary.neverSynthesized,
|
|
5050
|
+
stale: summary.stale,
|
|
5051
|
+
errors: summary.errors,
|
|
5052
|
+
locked: summary.locked,
|
|
5053
|
+
};
|
|
5016
5054
|
}
|
|
5017
5055
|
catch {
|
|
5018
5056
|
// Watcher unreachable — phase summary unavailable
|
|
@@ -5041,6 +5079,7 @@ function registerStatusRoute(app, deps) {
|
|
|
5041
5079
|
},
|
|
5042
5080
|
phaseStateSummary,
|
|
5043
5081
|
nextPhase,
|
|
5082
|
+
metaCounts,
|
|
5044
5083
|
};
|
|
5045
5084
|
},
|
|
5046
5085
|
});
|
|
@@ -5350,11 +5389,10 @@ class HttpWatcherClient {
|
|
|
5350
5389
|
async post(endpoint, body) {
|
|
5351
5390
|
const url = this.baseUrl + endpoint;
|
|
5352
5391
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
5353
|
-
const res = await
|
|
5392
|
+
const res = await fetchWithTimeout(url, this.timeoutMs, {
|
|
5354
5393
|
method: 'POST',
|
|
5355
5394
|
headers: { 'Content-Type': 'application/json' },
|
|
5356
5395
|
body: JSON.stringify(body),
|
|
5357
|
-
signal: AbortSignal.timeout(this.timeoutMs),
|
|
5358
5396
|
});
|
|
5359
5397
|
if (res.ok) {
|
|
5360
5398
|
return res.json();
|
|
@@ -5396,25 +5434,6 @@ class HttpWatcherClient {
|
|
|
5396
5434
|
*
|
|
5397
5435
|
* @module bootstrap
|
|
5398
5436
|
*/
|
|
5399
|
-
/**
|
|
5400
|
-
* Compute per-cycle token total from a completed meta.
|
|
5401
|
-
*
|
|
5402
|
-
* Exported for testing.
|
|
5403
|
-
*
|
|
5404
|
-
* Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
|
|
5405
|
-
* a value of 1 means architect ran this cycle (was 0 pre-increment),
|
|
5406
|
-
* so all three phase token fields are summed. A value \> 1 means architect
|
|
5407
|
-
* was skipped (cached brief reused), so only builder + critic are summed.
|
|
5408
|
-
*/
|
|
5409
|
-
function computeCycleTokens(meta) {
|
|
5410
|
-
const builderTokens = meta._builderTokens ?? 0;
|
|
5411
|
-
const criticTokens = meta._criticTokens ?? 0;
|
|
5412
|
-
const architectRan = (meta._synthesisCount ?? 1) === 1;
|
|
5413
|
-
const architectTokens = architectRan
|
|
5414
|
-
? (meta._architectTokens ?? 0)
|
|
5415
|
-
: 0;
|
|
5416
|
-
return architectTokens + builderTokens + criticTokens;
|
|
5417
|
-
}
|
|
5418
5437
|
/**
|
|
5419
5438
|
* Bootstrap the service: create logger, build server, start listening,
|
|
5420
5439
|
* wire scheduler, queue processing, rule registration, config hot-reload,
|
|
@@ -5491,16 +5510,7 @@ async function startService(config, configPath) {
|
|
|
5491
5510
|
// Track whether synthesis_start has been emitted (deferred until
|
|
5492
5511
|
// a phase actually begins, avoiding orphan "Started" messages
|
|
5493
5512
|
// when the meta is skipped/locked/fresh). See #165.
|
|
5494
|
-
let synthesisStartEmitted = false;
|
|
5495
5513
|
const result = await orchestratePhase(config, executor, watcher, path, async (evt) => {
|
|
5496
|
-
// Emit synthesis_start on first phase_start (deferred guard)
|
|
5497
|
-
if (evt.type === 'phase_start' && !synthesisStartEmitted) {
|
|
5498
|
-
synthesisStartEmitted = true;
|
|
5499
|
-
await progress.report({
|
|
5500
|
-
type: 'synthesis_start',
|
|
5501
|
-
path: ownerPath,
|
|
5502
|
-
});
|
|
5503
|
-
}
|
|
5504
5514
|
// Wire current-phase tracking for GET /queue and POST /synthesize/abort
|
|
5505
5515
|
if (evt.type === 'phase_start' && evt.phase) {
|
|
5506
5516
|
queue.setCurrentPhase(ownerPath, evt.phase);
|
|
@@ -5542,19 +5552,8 @@ async function startService(config, configPath) {
|
|
|
5542
5552
|
// Task #9: Reset backoff on ANY successful phase execution
|
|
5543
5553
|
scheduler.resetBackoff();
|
|
5544
5554
|
}
|
|
5545
|
-
//
|
|
5546
|
-
|
|
5547
|
-
const updatedMeta = result.phaseResult?.updatedMeta;
|
|
5548
|
-
const tokens = updatedMeta
|
|
5549
|
-
? computeCycleTokens(updatedMeta)
|
|
5550
|
-
: undefined;
|
|
5551
|
-
await progress.report({
|
|
5552
|
-
type: 'synthesis_complete',
|
|
5553
|
-
path: ownerPath,
|
|
5554
|
-
durationMs,
|
|
5555
|
-
tokens,
|
|
5556
|
-
});
|
|
5557
|
-
}
|
|
5555
|
+
// Note: synthesis_complete is no longer emitted; the phaseEnd template
|
|
5556
|
+
// for the final critic phase serves as the completion signal (#129).
|
|
5558
5557
|
}
|
|
5559
5558
|
catch (err) {
|
|
5560
5559
|
stats.totalErrors++;
|
|
@@ -5795,4 +5794,4 @@ const metaJsonSchema = z
|
|
|
5795
5794
|
})
|
|
5796
5795
|
.loose();
|
|
5797
5796
|
|
|
5798
|
-
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode,
|
|
5797
|
+
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, metaDescriptor, metaJsonSchema, migrateConfigPath, orchestratePhase, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, renderProgressEvent, resolveConfigPath, resolveMetaDir, runArchitect, runBuilder, runCritic, serviceConfigSchema, startService, toMetaError, verifyRuleApplication };
|
package/dist/progress/index.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
|
|
3
3
|
*
|
|
4
|
+
* Progress events are rendered using Handlebars templates. Owner-path links
|
|
5
|
+
* are resolved via the jeeves-server resolve-path API, with graceful
|
|
6
|
+
* fallback to raw filesystem paths when the server is unreachable.
|
|
7
|
+
*
|
|
4
8
|
* @module progress
|
|
5
9
|
*/
|
|
6
10
|
import type { Logger } from 'pino';
|
|
7
11
|
export type ProgressPhase = 'architect' | 'builder' | 'critic';
|
|
8
12
|
export type ProgressEvent = {
|
|
9
|
-
type: '
|
|
13
|
+
type: 'phase_start' | 'phase_complete' | 'error';
|
|
10
14
|
/** Owner path (not .meta path) of the entity being synthesized. */
|
|
11
15
|
path: string;
|
|
12
16
|
phase?: ProgressPhase;
|
|
@@ -14,6 +18,18 @@ export type ProgressEvent = {
|
|
|
14
18
|
durationMs?: number;
|
|
15
19
|
error?: string;
|
|
16
20
|
};
|
|
21
|
+
/** Compiled Handlebars templates for the three progress event types. */
|
|
22
|
+
type CompiledTemplates = {
|
|
23
|
+
phaseStart: HandlebarsTemplateDelegate;
|
|
24
|
+
phaseEnd: HandlebarsTemplateDelegate;
|
|
25
|
+
phaseError: HandlebarsTemplateDelegate;
|
|
26
|
+
};
|
|
27
|
+
/** Raw (string) template config — mirrors the schema definition. */
|
|
28
|
+
export type TemplateStrings = {
|
|
29
|
+
phaseStart: string;
|
|
30
|
+
phaseEnd: string;
|
|
31
|
+
phaseError: string;
|
|
32
|
+
};
|
|
17
33
|
export type ProgressReporterConfig = {
|
|
18
34
|
gatewayUrl: string;
|
|
19
35
|
gatewayApiKey?: string;
|
|
@@ -26,19 +42,32 @@ export type ProgressReporterConfig = {
|
|
|
26
42
|
reportChannel?: string;
|
|
27
43
|
/** Channel/user ID to send messages to. Takes priority over reportChannel as target. */
|
|
28
44
|
reportTarget?: string;
|
|
29
|
-
/** Optional base URL for the service, used to construct entity links. */
|
|
30
|
-
serverBaseUrl?: string;
|
|
31
45
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
46
|
+
* URL of the local jeeves-server instance, used to resolve filesystem paths
|
|
47
|
+
* to browse links via the resolve-path API.
|
|
48
|
+
* Default: http://127.0.0.1:1934
|
|
35
49
|
*/
|
|
36
|
-
|
|
50
|
+
serverUrl?: string;
|
|
51
|
+
/** Handlebars template strings for each progress event type. */
|
|
52
|
+
templates?: Partial<TemplateStrings>;
|
|
37
53
|
};
|
|
38
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Render the appropriate template for a progress event.
|
|
56
|
+
*
|
|
57
|
+
* @param event - The progress event to render.
|
|
58
|
+
* @param templates - Compiled Handlebars templates.
|
|
59
|
+
* @param serverUrl - jeeves-server URL for resolve-path API.
|
|
60
|
+
* @returns Rendered message string.
|
|
61
|
+
*/
|
|
62
|
+
export declare function renderProgressEvent(event: ProgressEvent, templates: CompiledTemplates, serverUrl: string): Promise<string>;
|
|
39
63
|
export declare class ProgressReporter {
|
|
40
64
|
private readonly config;
|
|
41
65
|
private readonly logger;
|
|
66
|
+
private templates;
|
|
67
|
+
/** Snapshot of template source strings used to compile `this.templates`. */
|
|
68
|
+
private lastTemplateSource;
|
|
69
|
+
private readonly serverUrl;
|
|
42
70
|
constructor(config: ProgressReporterConfig, logger: Logger);
|
|
43
71
|
report(event: ProgressEvent): Promise<void>;
|
|
44
72
|
}
|
|
73
|
+
export {};
|
package/dist/schema/config.d.ts
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
import { type MetaConfig, metaConfigSchema } from '@karmaniverous/jeeves-meta-core';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
export { type MetaConfig, metaConfigSchema };
|
|
11
|
+
/** Default Handlebars template strings for progress reporting. Single source of truth. */
|
|
12
|
+
export declare const DEFAULT_TEMPLATE_STRINGS: {
|
|
13
|
+
readonly phaseStart: ":gear: Started meta synthesis {{phase}} phase of <{{dirLink}}>";
|
|
14
|
+
readonly phaseEnd: ":white_check_mark: Completed meta synthesis {{phase}} phase ({{tokens}} tokens / {{seconds}}s) at <{{metaLink}}>";
|
|
15
|
+
readonly phaseError: ":x: Meta synthesis {{phase}} phase failed at <{{dirLink}}>\n Error: {{error}}";
|
|
16
|
+
};
|
|
11
17
|
/** Zod schema for a single auto-seed policy rule. */
|
|
12
18
|
declare const autoSeedRuleSchema: z.ZodObject<{
|
|
13
19
|
match: z.ZodString;
|
|
@@ -34,8 +40,12 @@ export declare const serviceConfigSchema: z.ZodObject<{
|
|
|
34
40
|
schedule: z.ZodDefault<z.ZodString>;
|
|
35
41
|
reportChannel: z.ZodOptional<z.ZodString>;
|
|
36
42
|
reportTarget: z.ZodOptional<z.ZodString>;
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
serverUrl: z.ZodDefault<z.ZodString>;
|
|
44
|
+
templates: z.ZodDefault<z.ZodObject<{
|
|
45
|
+
phaseStart: z.ZodDefault<z.ZodString>;
|
|
46
|
+
phaseEnd: z.ZodDefault<z.ZodString>;
|
|
47
|
+
phaseError: z.ZodDefault<z.ZodString>;
|
|
48
|
+
}, z.core.$strip>>;
|
|
39
49
|
watcherHealthIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
40
50
|
logging: z.ZodDefault<z.ZodObject<{
|
|
41
51
|
level: z.ZodDefault<z.ZodString>;
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module schema
|
|
5
5
|
*/
|
|
6
|
-
export { type AutoSeedRule, type MetaConfig, metaConfigSchema, type ServiceConfig, serviceConfigSchema, } from './config.js';
|
|
6
|
+
export { type AutoSeedRule, DEFAULT_TEMPLATE_STRINGS, type MetaConfig, metaConfigSchema, type ServiceConfig, serviceConfigSchema, } from './config.js';
|
|
7
7
|
export { type MetaError, metaErrorSchema } from './error.js';
|
|
8
8
|
export { type MetaJson, metaJsonSchema, type PhaseName, phaseNames, type PhaseState, phaseStateSchema, type PhaseStatus, phaseStatuses, } from './meta.js';
|
package/package.json
CHANGED