@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/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';
|
|
@@ -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. */
|
|
@@ -1254,6 +1252,12 @@ function applyHotReloadedConfig(newConfig) {
|
|
|
1254
1252
|
*
|
|
1255
1253
|
* @module schema/config
|
|
1256
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
|
+
};
|
|
1257
1261
|
/** Zod schema for logging configuration. */
|
|
1258
1262
|
const loggingSchema = z.object({
|
|
1259
1263
|
/** Log level. */
|
|
@@ -1282,8 +1286,24 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
1282
1286
|
reportChannel: z.string().optional(),
|
|
1283
1287
|
/** Channel/user ID to send progress messages to. */
|
|
1284
1288
|
reportTarget: z.string().optional(),
|
|
1285
|
-
/**
|
|
1286
|
-
|
|
1289
|
+
/**
|
|
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).
|
|
1299
|
+
*/
|
|
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 })),
|
|
1287
1307
|
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
1288
1308
|
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
1289
1309
|
/** Logging configuration. */
|
|
@@ -1308,7 +1328,7 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
1308
1328
|
/**
|
|
1309
1329
|
* Load and resolve jeeves-meta service config.
|
|
1310
1330
|
*
|
|
1311
|
-
* Supports
|
|
1331
|
+
* Supports environment-variable substitution (dollar-brace pattern).
|
|
1312
1332
|
*
|
|
1313
1333
|
* @module configLoader
|
|
1314
1334
|
*/
|
|
@@ -1340,19 +1360,6 @@ function substituteEnvVars(value) {
|
|
|
1340
1360
|
}
|
|
1341
1361
|
return value;
|
|
1342
1362
|
}
|
|
1343
|
-
/**
|
|
1344
|
-
* Resolve \@file: references in a config value.
|
|
1345
|
-
*
|
|
1346
|
-
* @param value - String value that may start with "\@file:".
|
|
1347
|
-
* @param baseDir - Base directory for resolving relative paths.
|
|
1348
|
-
* @returns The resolved string (file contents or original value).
|
|
1349
|
-
*/
|
|
1350
|
-
function resolveFileRef(value, baseDir) {
|
|
1351
|
-
if (!value.startsWith('@file:'))
|
|
1352
|
-
return value;
|
|
1353
|
-
const filePath = join(baseDir, value.slice(6));
|
|
1354
|
-
return readFileSync(filePath, 'utf8');
|
|
1355
|
-
}
|
|
1356
1363
|
/**
|
|
1357
1364
|
* Migrate legacy config path to the new canonical location.
|
|
1358
1365
|
*
|
|
@@ -1401,8 +1408,7 @@ function resolveConfigPath(args) {
|
|
|
1401
1408
|
/**
|
|
1402
1409
|
* Load service config from a JSON file.
|
|
1403
1410
|
*
|
|
1404
|
-
*
|
|
1405
|
-
* and substitutes environment-variable placeholders throughout.
|
|
1411
|
+
* Substitutes environment-variable placeholders throughout.
|
|
1406
1412
|
*
|
|
1407
1413
|
* @param configPath - Path to config JSON file.
|
|
1408
1414
|
* @returns Validated ServiceConfig.
|
|
@@ -1410,13 +1416,6 @@ function resolveConfigPath(args) {
|
|
|
1410
1416
|
function loadServiceConfig(configPath) {
|
|
1411
1417
|
const rawText = readFileSync(configPath, 'utf8');
|
|
1412
1418
|
const raw = substituteEnvVars(JSON.parse(rawText));
|
|
1413
|
-
const baseDir = dirname(configPath);
|
|
1414
|
-
if (typeof raw['defaultArchitect'] === 'string') {
|
|
1415
|
-
raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
|
|
1416
|
-
}
|
|
1417
|
-
if (typeof raw['defaultCritic'] === 'string') {
|
|
1418
|
-
raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
|
|
1419
|
-
}
|
|
1420
1419
|
return serviceConfigSchema.parse(raw);
|
|
1421
1420
|
}
|
|
1422
1421
|
|
|
@@ -1792,9 +1791,6 @@ function createLogger(config) {
|
|
|
1792
1791
|
* Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
|
|
1793
1792
|
* Loaded at runtime relative to the compiled module location.
|
|
1794
1793
|
*
|
|
1795
|
-
* Users can override via `defaultArchitect` / `defaultCritic` in the service
|
|
1796
|
-
* config. Most installations should use the built-in defaults.
|
|
1797
|
-
*
|
|
1798
1794
|
* @module prompts
|
|
1799
1795
|
*/
|
|
1800
1796
|
const packageRoot = packageDirectorySync({
|
|
@@ -2094,7 +2090,7 @@ function buildArchitectTask(ctx, meta, config) {
|
|
|
2094
2090
|
const sections = [
|
|
2095
2091
|
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
2096
2092
|
'',
|
|
2097
|
-
|
|
2093
|
+
DEFAULT_ARCHITECT_PROMPT,
|
|
2098
2094
|
'',
|
|
2099
2095
|
'## SCOPE',
|
|
2100
2096
|
`Path: ${ctx.path}`,
|
|
@@ -2143,7 +2139,7 @@ function buildBuilderTask(ctx, meta, config) {
|
|
|
2143
2139
|
includeSteer: false,
|
|
2144
2140
|
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
2145
2141
|
});
|
|
2146
|
-
sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.');
|
|
2142
|
+
sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.', '', 'IMPORTANT: Do not call sessions_spawn or sessions_yield. Read all files directly using the tools', 'available in your session. Do not attempt to parallelize work by spawning sub-agents.');
|
|
2147
2143
|
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
2148
2144
|
}
|
|
2149
2145
|
/**
|
|
@@ -2158,7 +2154,7 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
2158
2154
|
const sections = [
|
|
2159
2155
|
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
2160
2156
|
'',
|
|
2161
|
-
|
|
2157
|
+
DEFAULT_CRITIC_PROMPT,
|
|
2162
2158
|
'',
|
|
2163
2159
|
'## SYNTHESIS TO EVALUATE',
|
|
2164
2160
|
meta._content ?? '(No content produced)',
|
|
@@ -2575,8 +2571,8 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
|
|
|
2575
2571
|
// through real builder cycles, architect runs with the current prompt and
|
|
2576
2572
|
// the snapshot updates. Coupling prompt changes to invalidation causes a
|
|
2577
2573
|
// corpus-wide synthesis storm (see #163).
|
|
2578
|
-
const architectChanged = isPromptStale(meta._architect,
|
|
2579
|
-
const criticChanged = isPromptStale(meta._critic,
|
|
2574
|
+
const architectChanged = isPromptStale(meta._architect, DEFAULT_ARCHITECT_PROMPT);
|
|
2575
|
+
const criticChanged = isPromptStale(meta._critic, DEFAULT_CRITIC_PROMPT);
|
|
2580
2576
|
const effectiveSynthesisCount = meta._synthesisCount ?? 0;
|
|
2581
2577
|
// _crossRefs declaration change
|
|
2582
2578
|
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
@@ -2823,10 +2819,11 @@ function parseArchitectOutput(output) {
|
|
|
2823
2819
|
/**
|
|
2824
2820
|
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
2825
2821
|
*
|
|
2826
|
-
* Attempts JSON
|
|
2822
|
+
* Attempts JSON extraction via multiple strategies. Returns null if all strategies fail
|
|
2823
|
+
* (e.g. the output is plain text, self-talk, or a sub-agent delegation response).
|
|
2827
2824
|
*
|
|
2828
2825
|
* @param output - Raw subprocess output.
|
|
2829
|
-
* @returns Parsed builder output
|
|
2826
|
+
* @returns Parsed builder output, or null if output is not valid builder JSON.
|
|
2830
2827
|
*/
|
|
2831
2828
|
function parseBuilderOutput(output) {
|
|
2832
2829
|
const trimmed = stripSentinel(output);
|
|
@@ -2855,8 +2852,8 @@ function parseBuilderOutput(output) {
|
|
|
2855
2852
|
if (result)
|
|
2856
2853
|
return result;
|
|
2857
2854
|
}
|
|
2858
|
-
//
|
|
2859
|
-
return
|
|
2855
|
+
// All JSON strategies failed — not valid builder output
|
|
2856
|
+
return null;
|
|
2860
2857
|
}
|
|
2861
2858
|
/** Try to parse a string as JSON and extract builder output fields. */
|
|
2862
2859
|
function tryParseJson(str) {
|
|
@@ -2984,7 +2981,7 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
2984
2981
|
ps = architectSuccess(ps);
|
|
2985
2982
|
const architectUpdates = {
|
|
2986
2983
|
_builder: builderBrief,
|
|
2987
|
-
_architect:
|
|
2984
|
+
_architect: DEFAULT_ARCHITECT_PROMPT,
|
|
2988
2985
|
_synthesisCount: 0,
|
|
2989
2986
|
_architectTokens: architectTokens,
|
|
2990
2987
|
_generatedAt: new Date().toISOString(),
|
|
@@ -3056,6 +3053,12 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
3056
3053
|
return { executed: true, phaseState: ps, updatedMeta };
|
|
3057
3054
|
}
|
|
3058
3055
|
const builderOutput = parseBuilderOutput(rawOutput);
|
|
3056
|
+
if (!builderOutput) {
|
|
3057
|
+
throw new Error('Builder output is not valid JSON — expected { _content|content: string, ... }. ' +
|
|
3058
|
+
'Raw output (' +
|
|
3059
|
+
rawOutput.length.toString() +
|
|
3060
|
+
' chars) did not match any JSON extraction strategy.');
|
|
3061
|
+
}
|
|
3059
3062
|
const builderTokens = result.tokens;
|
|
3060
3063
|
// Builder success: builder → fresh, critic → pending
|
|
3061
3064
|
ps = builderSuccess(ps);
|
|
@@ -3087,7 +3090,8 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
3087
3090
|
try {
|
|
3088
3091
|
const raw = await readFile(err.outputPath, 'utf8');
|
|
3089
3092
|
const partial = parseBuilderOutput(raw);
|
|
3090
|
-
if (partial
|
|
3093
|
+
if (partial !== null &&
|
|
3094
|
+
partial.state !== undefined &&
|
|
3091
3095
|
JSON.stringify(partial.state) !== JSON.stringify(currentMeta._state)) {
|
|
3092
3096
|
partialState = { _state: partial.state };
|
|
3093
3097
|
}
|
|
@@ -3130,7 +3134,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
3130
3134
|
const cycleComplete = isFullyFresh(ps);
|
|
3131
3135
|
const updates = {
|
|
3132
3136
|
_feedback: feedback,
|
|
3133
|
-
_critic:
|
|
3137
|
+
_critic: DEFAULT_CRITIC_PROMPT,
|
|
3134
3138
|
_criticTokens: criticTokens,
|
|
3135
3139
|
_error: undefined,
|
|
3136
3140
|
};
|
|
@@ -3325,95 +3329,118 @@ async function executePhase(node, currentMeta, phaseState, phase, config, execut
|
|
|
3325
3329
|
/**
|
|
3326
3330
|
* Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
|
|
3327
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
|
+
*
|
|
3328
3336
|
* @module progress
|
|
3329
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
|
+
}
|
|
3330
3351
|
function formatNumber(n) {
|
|
3331
3352
|
return n.toLocaleString('en-US');
|
|
3332
3353
|
}
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
function
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
.
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
/** Build a link (or plain path) to the owner directory. */
|
|
3356
|
-
function buildDirectoryLink(path, serverBaseUrl) {
|
|
3357
|
-
const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
|
|
3358
|
-
const encoded = encodePathSegments(normalized);
|
|
3359
|
-
if (!serverBaseUrl)
|
|
3360
|
-
return normalized;
|
|
3361
|
-
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
3362
|
-
return `${base}/path${encoded}`;
|
|
3363
|
-
}
|
|
3364
|
-
/** Build a link (or plain path) to the entity's meta.json output file. */
|
|
3365
|
-
function buildMetaJsonLink(path, serverBaseUrl) {
|
|
3366
|
-
const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
|
|
3367
|
-
const metaJsonPath = `${normalized}/.meta/meta.json`;
|
|
3368
|
-
const encoded = encodePathSegments(metaJsonPath);
|
|
3369
|
-
if (!serverBaseUrl)
|
|
3370
|
-
return metaJsonPath;
|
|
3371
|
-
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
3372
|
-
return `${base}/path${encoded}`;
|
|
3373
|
-
}
|
|
3374
|
-
function formatProgressEvent(event, serverBaseUrl) {
|
|
3375
|
-
switch (event.type) {
|
|
3376
|
-
case 'synthesis_start': {
|
|
3377
|
-
const dirLink = buildDirectoryLink(event.path, serverBaseUrl);
|
|
3378
|
-
return `🔬 Started meta synthesis: ${dirLink}`;
|
|
3379
|
-
}
|
|
3380
|
-
case 'phase_start': {
|
|
3381
|
-
if (!event.phase) {
|
|
3382
|
-
return ' ⚙️ Phase started';
|
|
3383
|
-
}
|
|
3384
|
-
return ` ⚙️ ${titleCasePhase(event.phase)} phase started`;
|
|
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;
|
|
3385
3376
|
}
|
|
3377
|
+
return fsPath;
|
|
3378
|
+
}
|
|
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
|
+
};
|
|
3410
|
+
switch (event.type) {
|
|
3411
|
+
case 'phase_start':
|
|
3412
|
+
return templates.phaseStart(base);
|
|
3386
3413
|
case 'phase_complete': {
|
|
3387
|
-
const
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
case 'synthesis_complete': {
|
|
3393
|
-
const metaLink = buildMetaJsonLink(event.path, serverBaseUrl);
|
|
3394
|
-
const tokenStr = formatTokens(event.tokens);
|
|
3395
|
-
const duration = event.durationMs !== undefined
|
|
3396
|
-
? formatSeconds(event.durationMs)
|
|
3397
|
-
: '0.0s';
|
|
3398
|
-
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 });
|
|
3399
3419
|
}
|
|
3400
3420
|
case 'error': {
|
|
3401
|
-
const
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
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 });
|
|
3405
3426
|
}
|
|
3406
|
-
default:
|
|
3427
|
+
default:
|
|
3407
3428
|
return 'Unknown progress event';
|
|
3408
|
-
}
|
|
3409
3429
|
}
|
|
3410
3430
|
}
|
|
3411
3431
|
class ProgressReporter {
|
|
3412
3432
|
config;
|
|
3413
3433
|
logger;
|
|
3434
|
+
templates;
|
|
3435
|
+
/** Snapshot of template source strings used to compile `this.templates`. */
|
|
3436
|
+
lastTemplateSource;
|
|
3437
|
+
serverUrl;
|
|
3414
3438
|
constructor(config, logger) {
|
|
3415
3439
|
this.config = config;
|
|
3416
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';
|
|
3417
3444
|
}
|
|
3418
3445
|
async report(event) {
|
|
3419
3446
|
// Multi-channel mode: reportTarget is the destination, reportChannel is the platform.
|
|
@@ -3421,7 +3448,27 @@ class ProgressReporter {
|
|
|
3421
3448
|
const target = this.config.reportTarget ?? this.config.reportChannel;
|
|
3422
3449
|
if (!target)
|
|
3423
3450
|
return;
|
|
3424
|
-
|
|
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
|
+
}
|
|
3425
3472
|
const url = new URL('/tools/invoke', this.config.gatewayUrl);
|
|
3426
3473
|
const args = {
|
|
3427
3474
|
action: 'send',
|
|
@@ -4971,6 +5018,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4971
5018
|
critic: emptyPhaseCounts(),
|
|
4972
5019
|
};
|
|
4973
5020
|
let nextPhase = null;
|
|
5021
|
+
let metaCounts = null;
|
|
4974
5022
|
try {
|
|
4975
5023
|
const metaResult = await cache.get(config, watcher);
|
|
4976
5024
|
// Count raw phase states (before retry) for display
|
|
@@ -4992,6 +5040,17 @@ function registerStatusRoute(app, deps) {
|
|
|
4992
5040
|
staleness: winner.effectiveStaleness,
|
|
4993
5041
|
};
|
|
4994
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
|
+
};
|
|
4995
5054
|
}
|
|
4996
5055
|
catch {
|
|
4997
5056
|
// Watcher unreachable — phase summary unavailable
|
|
@@ -5020,6 +5079,7 @@ function registerStatusRoute(app, deps) {
|
|
|
5020
5079
|
},
|
|
5021
5080
|
phaseStateSummary,
|
|
5022
5081
|
nextPhase,
|
|
5082
|
+
metaCounts,
|
|
5023
5083
|
};
|
|
5024
5084
|
},
|
|
5025
5085
|
});
|
|
@@ -5329,11 +5389,10 @@ class HttpWatcherClient {
|
|
|
5329
5389
|
async post(endpoint, body) {
|
|
5330
5390
|
const url = this.baseUrl + endpoint;
|
|
5331
5391
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
5332
|
-
const res = await
|
|
5392
|
+
const res = await fetchWithTimeout(url, this.timeoutMs, {
|
|
5333
5393
|
method: 'POST',
|
|
5334
5394
|
headers: { 'Content-Type': 'application/json' },
|
|
5335
5395
|
body: JSON.stringify(body),
|
|
5336
|
-
signal: AbortSignal.timeout(this.timeoutMs),
|
|
5337
5396
|
});
|
|
5338
5397
|
if (res.ok) {
|
|
5339
5398
|
return res.json();
|
|
@@ -5375,25 +5434,6 @@ class HttpWatcherClient {
|
|
|
5375
5434
|
*
|
|
5376
5435
|
* @module bootstrap
|
|
5377
5436
|
*/
|
|
5378
|
-
/**
|
|
5379
|
-
* Compute per-cycle token total from a completed meta.
|
|
5380
|
-
*
|
|
5381
|
-
* Exported for testing.
|
|
5382
|
-
*
|
|
5383
|
-
* Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
|
|
5384
|
-
* a value of 1 means architect ran this cycle (was 0 pre-increment),
|
|
5385
|
-
* so all three phase token fields are summed. A value \> 1 means architect
|
|
5386
|
-
* was skipped (cached brief reused), so only builder + critic are summed.
|
|
5387
|
-
*/
|
|
5388
|
-
function computeCycleTokens(meta) {
|
|
5389
|
-
const builderTokens = meta._builderTokens ?? 0;
|
|
5390
|
-
const criticTokens = meta._criticTokens ?? 0;
|
|
5391
|
-
const architectRan = (meta._synthesisCount ?? 1) === 1;
|
|
5392
|
-
const architectTokens = architectRan
|
|
5393
|
-
? (meta._architectTokens ?? 0)
|
|
5394
|
-
: 0;
|
|
5395
|
-
return architectTokens + builderTokens + criticTokens;
|
|
5396
|
-
}
|
|
5397
5437
|
/**
|
|
5398
5438
|
* Bootstrap the service: create logger, build server, start listening,
|
|
5399
5439
|
* wire scheduler, queue processing, rule registration, config hot-reload,
|
|
@@ -5470,16 +5510,7 @@ async function startService(config, configPath) {
|
|
|
5470
5510
|
// Track whether synthesis_start has been emitted (deferred until
|
|
5471
5511
|
// a phase actually begins, avoiding orphan "Started" messages
|
|
5472
5512
|
// when the meta is skipped/locked/fresh). See #165.
|
|
5473
|
-
let synthesisStartEmitted = false;
|
|
5474
5513
|
const result = await orchestratePhase(config, executor, watcher, path, async (evt) => {
|
|
5475
|
-
// Emit synthesis_start on first phase_start (deferred guard)
|
|
5476
|
-
if (evt.type === 'phase_start' && !synthesisStartEmitted) {
|
|
5477
|
-
synthesisStartEmitted = true;
|
|
5478
|
-
await progress.report({
|
|
5479
|
-
type: 'synthesis_start',
|
|
5480
|
-
path: ownerPath,
|
|
5481
|
-
});
|
|
5482
|
-
}
|
|
5483
5514
|
// Wire current-phase tracking for GET /queue and POST /synthesize/abort
|
|
5484
5515
|
if (evt.type === 'phase_start' && evt.phase) {
|
|
5485
5516
|
queue.setCurrentPhase(ownerPath, evt.phase);
|
|
@@ -5521,19 +5552,8 @@ async function startService(config, configPath) {
|
|
|
5521
5552
|
// Task #9: Reset backoff on ANY successful phase execution
|
|
5522
5553
|
scheduler.resetBackoff();
|
|
5523
5554
|
}
|
|
5524
|
-
//
|
|
5525
|
-
|
|
5526
|
-
const updatedMeta = result.phaseResult?.updatedMeta;
|
|
5527
|
-
const tokens = updatedMeta
|
|
5528
|
-
? computeCycleTokens(updatedMeta)
|
|
5529
|
-
: undefined;
|
|
5530
|
-
await progress.report({
|
|
5531
|
-
type: 'synthesis_complete',
|
|
5532
|
-
path: ownerPath,
|
|
5533
|
-
durationMs,
|
|
5534
|
-
tokens,
|
|
5535
|
-
});
|
|
5536
|
-
}
|
|
5555
|
+
// Note: synthesis_complete is no longer emitted; the phaseEnd template
|
|
5556
|
+
// for the final critic phase serves as the completion signal (#129).
|
|
5537
5557
|
}
|
|
5538
5558
|
catch (err) {
|
|
5539
5559
|
stats.totalErrors++;
|
|
@@ -5774,4 +5794,4 @@ const metaJsonSchema = z
|
|
|
5774
5794
|
})
|
|
5775
5795
|
.loose();
|
|
5776
5796
|
|
|
5777
|
-
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 };
|
|
@@ -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
|
@@ -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,13 +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
|
-
/**
|
|
30
|
-
|
|
45
|
+
/**
|
|
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
|
|
49
|
+
*/
|
|
50
|
+
serverUrl?: string;
|
|
51
|
+
/** Handlebars template strings for each progress event type. */
|
|
52
|
+
templates?: Partial<TemplateStrings>;
|
|
31
53
|
};
|
|
32
|
-
|
|
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>;
|
|
33
63
|
export declare class ProgressReporter {
|
|
34
64
|
private readonly config;
|
|
35
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;
|
|
36
70
|
constructor(config: ProgressReporterConfig, logger: Logger);
|
|
37
71
|
report(event: ProgressEvent): Promise<void>;
|
|
38
72
|
}
|
|
73
|
+
export {};
|
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. */
|