@karmaniverous/jeeves-meta 0.15.2 → 0.15.4
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/architect.md +17 -0
- package/dist/cli/jeeves-meta/index.js +887 -817
- package/dist/index.d.ts +343 -423
- package/dist/index.js +1514 -1818
- package/dist/prompts/architect.md +17 -0
- package/package.json +3 -3
|
@@ -15,9 +15,9 @@ import * as commander from 'commander';
|
|
|
15
15
|
import { execSync } from 'node:child_process';
|
|
16
16
|
import { homedir, tmpdir } from 'node:os';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import Handlebars from 'handlebars';
|
|
18
19
|
import { readFile, unlink, mkdir, writeFile, copyFile } from 'node:fs/promises';
|
|
19
20
|
import pino from 'pino';
|
|
20
|
-
import Handlebars from 'handlebars';
|
|
21
21
|
import process$1 from 'node:process';
|
|
22
22
|
import { Cron } from 'croner';
|
|
23
23
|
import Fastify from 'fastify';
|
|
@@ -7239,6 +7239,11 @@ const workspaceCoreConfigSchema = z
|
|
|
7239
7239
|
configRoot: z.string().optional().describe('Platform config root path'),
|
|
7240
7240
|
/** OpenClaw gateway URL. */
|
|
7241
7241
|
gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
|
|
7242
|
+
/** Dev repo paths keyed by component name. */
|
|
7243
|
+
devRepos: z
|
|
7244
|
+
.record(z.string(), z.string())
|
|
7245
|
+
.optional()
|
|
7246
|
+
.describe('Dev repo paths by component name'),
|
|
7242
7247
|
})
|
|
7243
7248
|
.partial();
|
|
7244
7249
|
/** Memory shared config section. */
|
|
@@ -7253,13 +7258,6 @@ const workspaceMemoryConfigSchema = z
|
|
|
7253
7258
|
.max(1)
|
|
7254
7259
|
.optional()
|
|
7255
7260
|
.describe('Memory warning threshold'),
|
|
7256
|
-
/** Staleness threshold in days. */
|
|
7257
|
-
staleDays: z
|
|
7258
|
-
.number()
|
|
7259
|
-
.int()
|
|
7260
|
-
.positive()
|
|
7261
|
-
.optional()
|
|
7262
|
-
.describe('Memory staleness threshold in days'),
|
|
7263
7261
|
})
|
|
7264
7262
|
.partial();
|
|
7265
7263
|
/** Workspace config Zod schema. */
|
|
@@ -8167,16 +8165,115 @@ function createServiceCli(descriptor) {
|
|
|
8167
8165
|
});
|
|
8168
8166
|
// Apply custom CLI commands if provided
|
|
8169
8167
|
if (descriptor.customCliCommands) {
|
|
8170
|
-
// Cast required: @commander-js/extra-typings Command has generic type
|
|
8171
|
-
// parameters that don't align with the descriptor's base Command type.
|
|
8172
|
-
// The descriptor can't know the parent Command's exact generic parameters
|
|
8173
|
-
// at definition time. The cast is safe — customCliCommands only adds
|
|
8174
|
-
// subcommands and doesn't depend on the parent's generic state.
|
|
8175
8168
|
descriptor.customCliCommands(program);
|
|
8176
8169
|
}
|
|
8177
8170
|
return program;
|
|
8178
8171
|
}
|
|
8179
8172
|
|
|
8173
|
+
var toolsPlatformTemplate = `### Tool Hierarchy
|
|
8174
|
+
|
|
8175
|
+
When searching for information across indexed paths, **always use \`watcher_search\` before filesystem commands** (\`exec\`, \`grep\`, \`find\`). The semantic index covers the full indexed corpus and surfaces related files you may not have considered.
|
|
8176
|
+
|
|
8177
|
+
Use \`watcher_scan\` (no embeddings, no query string) for structural queries: file enumeration, staleness checks, domain listing, counts.
|
|
8178
|
+
|
|
8179
|
+
Direct filesystem access is for **acting on** search results, not bypassing them.
|
|
8180
|
+
|
|
8181
|
+
### Shell Scripting
|
|
8182
|
+
|
|
8183
|
+
Default to \`node -e\` or \`.js\` scripts for \`exec\` calls. PowerShell corrupts multi-byte UTF-8 characters and mangles escaping. Use PowerShell only for Windows service management, registry operations, and similar platform-specific tasks.
|
|
8184
|
+
|
|
8185
|
+
### File Bridge for External Repos
|
|
8186
|
+
|
|
8187
|
+
When editing files outside the workspace, use the bridge pattern: copy in → edit the workspace copy → bridge out. Never write temp patch scripts. The workspace is the authoritative working directory.
|
|
8188
|
+
|
|
8189
|
+
### Gateway Self-Destruction Warning
|
|
8190
|
+
|
|
8191
|
+
⚠️ Any command that stops the gateway **stops the assistant**. Never run \`openclaw gateway stop\` or \`openclaw gateway restart\` without explicit owner approval. When approved, it must be the **absolute last action** — all other work must be complete first, all messages sent, all files saved.
|
|
8192
|
+
|
|
8193
|
+
### Messaging
|
|
8194
|
+
|
|
8195
|
+
**Same-channel replies:** Don't use the \`message\` tool. It fires immediately, jumping ahead of streaming narration. Just write text as your response.
|
|
8196
|
+
|
|
8197
|
+
**Cross-channel sends:** Use the \`message\` tool with an explicit \`target\` to send to a different channel or DM.
|
|
8198
|
+
|
|
8199
|
+
### Plugin Lifecycle
|
|
8200
|
+
|
|
8201
|
+
\`\`\`bash
|
|
8202
|
+
# Platform bootstrap (content seeding)
|
|
8203
|
+
npx @karmaniverous/jeeves install
|
|
8204
|
+
|
|
8205
|
+
# Component plugin install
|
|
8206
|
+
npx @karmaniverous/jeeves-{component}-openclaw install
|
|
8207
|
+
|
|
8208
|
+
# Component plugin uninstall
|
|
8209
|
+
npx @karmaniverous/jeeves-{component}-openclaw uninstall
|
|
8210
|
+
|
|
8211
|
+
# Platform teardown (remove managed sections)
|
|
8212
|
+
npx @karmaniverous/jeeves uninstall
|
|
8213
|
+
\`\`\`
|
|
8214
|
+
|
|
8215
|
+
Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands above.
|
|
8216
|
+
|
|
8217
|
+
### Reference Templates
|
|
8218
|
+
|
|
8219
|
+
{{#if templatePath}}
|
|
8220
|
+
Reference templates are available at \`{{templatePath}}\`:
|
|
8221
|
+
|
|
8222
|
+
| Template | Purpose |
|
|
8223
|
+
|----------|---------|
|
|
8224
|
+
| \`spec.md\` | Skeleton for new product specifications — all section headers, decision format, dev plan format |
|
|
8225
|
+
| \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
|
|
8226
|
+
|
|
8227
|
+
Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
|
|
8228
|
+
{{else}}
|
|
8229
|
+
> Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
|
|
8230
|
+
{{/if}}
|
|
8231
|
+
|
|
8232
|
+
### Post-Upgrade Maintenance
|
|
8233
|
+
|
|
8234
|
+
After updating OpenClaw (\`npm install -g openclaw@latest\` or equivalent), reinstall all Jeeves component plugins to repair install state:
|
|
8235
|
+
|
|
8236
|
+
\`\`\`bash
|
|
8237
|
+
npx @karmaniverous/jeeves install
|
|
8238
|
+
npx @karmaniverous/jeeves-runner-openclaw install
|
|
8239
|
+
npx @karmaniverous/jeeves-watcher-openclaw install
|
|
8240
|
+
npx @karmaniverous/jeeves-server-openclaw install
|
|
8241
|
+
npx @karmaniverous/jeeves-meta-openclaw install
|
|
8242
|
+
\`\`\`
|
|
8243
|
+
|
|
8244
|
+
Then restart the gateway. Plugin installers copy dist files and patch config; reinstalling after an OpenClaw update ensures the extensions directory stays consistent.
|
|
8245
|
+
|
|
8246
|
+
### Source Code Preference
|
|
8247
|
+
|
|
8248
|
+
{{#if devRepos}}
|
|
8249
|
+
When investigating, debugging, or analyzing Jeeves components, always read TypeScript source from dev repos — never compiled \`dist/\` from the global npm install. Dev repos:
|
|
8250
|
+
|
|
8251
|
+
| Component | Dev Repo |
|
|
8252
|
+
|-----------|----------|
|
|
8253
|
+
{{#each devRepos}}
|
|
8254
|
+
| {{@key}} | \`{{this}}\` |
|
|
8255
|
+
{{/each}}
|
|
8256
|
+
|
|
8257
|
+
Built code is minified, harder to reason about, and wastes context. Always \`git pull\` before analysis.
|
|
8258
|
+
{{else}}
|
|
8259
|
+
> Dev repo paths not configured. Add \`core.devRepos\` to \`jeeves.config.json\` to enable source code preference guidance.
|
|
8260
|
+
{{/if}}
|
|
8261
|
+
`;
|
|
8262
|
+
|
|
8263
|
+
/**
|
|
8264
|
+
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
8265
|
+
*
|
|
8266
|
+
* @remarks
|
|
8267
|
+
* Called by `ComponentWriter` on each cycle. Not directly exposed to components.
|
|
8268
|
+
* Reads content files from the package's `content/` directory, renders the
|
|
8269
|
+
* Platform template with live data, and writes managed sections using
|
|
8270
|
+
* `updateManagedSection`.
|
|
8271
|
+
*/
|
|
8272
|
+
/** Compiled Handlebars template for the Platform section (cached at module level). */
|
|
8273
|
+
Handlebars.compile(toolsPlatformTemplate, {
|
|
8274
|
+
noEscape: true,
|
|
8275
|
+
});
|
|
8276
|
+
|
|
8180
8277
|
/**
|
|
8181
8278
|
* Core configuration schema and resolution.
|
|
8182
8279
|
*
|
|
@@ -8273,10 +8370,10 @@ function getServiceUrl(serviceName, consumerName) {
|
|
|
8273
8370
|
if (coreUrl)
|
|
8274
8371
|
return coreUrl;
|
|
8275
8372
|
// 3. Fall back to port constants
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
return `http://127.0.0.1:${String(port)}`;
|
|
8373
|
+
if (serviceName in DEFAULT_PORTS) {
|
|
8374
|
+
return `http://127.0.0.1:${String(DEFAULT_PORTS[serviceName])}`;
|
|
8279
8375
|
}
|
|
8376
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
8280
8377
|
}
|
|
8281
8378
|
|
|
8282
8379
|
/**
|
|
@@ -8322,323 +8419,107 @@ function sleepAsync(ms) {
|
|
|
8322
8419
|
}
|
|
8323
8420
|
|
|
8324
8421
|
/**
|
|
8325
|
-
*
|
|
8422
|
+
* Compute summary statistics from an array of MetaEntry objects.
|
|
8326
8423
|
*
|
|
8327
|
-
*
|
|
8328
|
-
* via the component descriptor's onConfigApply callback.
|
|
8424
|
+
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
8329
8425
|
*
|
|
8330
|
-
* @module
|
|
8426
|
+
* @module discovery/computeSummary
|
|
8331
8427
|
*/
|
|
8332
8428
|
/**
|
|
8333
|
-
*
|
|
8429
|
+
* Compute summary statistics from a list of meta entries.
|
|
8334
8430
|
*
|
|
8335
|
-
*
|
|
8336
|
-
*
|
|
8431
|
+
* @param entries - Enriched meta entries (full or filtered).
|
|
8432
|
+
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
8433
|
+
* @returns Aggregated summary statistics.
|
|
8337
8434
|
*/
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
let
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
config.logging.level = newConfig.logging.level;
|
|
8371
|
-
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
8372
|
-
}
|
|
8373
|
-
const restartSet = new Set(RESTART_REQUIRED_FIELDS);
|
|
8374
|
-
for (const key of Object.keys(newConfig)) {
|
|
8375
|
-
if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
|
|
8376
|
-
continue;
|
|
8435
|
+
function computeSummary(entries, depthWeight) {
|
|
8436
|
+
let staleCount = 0;
|
|
8437
|
+
let errorCount = 0;
|
|
8438
|
+
let lockedCount = 0;
|
|
8439
|
+
let disabledCount = 0;
|
|
8440
|
+
let neverSynthesizedCount = 0;
|
|
8441
|
+
let totalArchitectTokens = 0;
|
|
8442
|
+
let totalBuilderTokens = 0;
|
|
8443
|
+
let totalCriticTokens = 0;
|
|
8444
|
+
let stalestPath = null;
|
|
8445
|
+
let stalestEffective = -1;
|
|
8446
|
+
let lastSynthesizedPath = null;
|
|
8447
|
+
let lastSynthesizedAt = null;
|
|
8448
|
+
for (const e of entries) {
|
|
8449
|
+
if (e.stalenessSeconds > 0)
|
|
8450
|
+
staleCount++;
|
|
8451
|
+
if (e.hasError)
|
|
8452
|
+
errorCount++;
|
|
8453
|
+
if (e.locked)
|
|
8454
|
+
lockedCount++;
|
|
8455
|
+
if (e.disabled)
|
|
8456
|
+
disabledCount++;
|
|
8457
|
+
if (e.lastSynthesized === null)
|
|
8458
|
+
neverSynthesizedCount++;
|
|
8459
|
+
totalArchitectTokens += e.architectTokens ?? 0;
|
|
8460
|
+
totalBuilderTokens += e.builderTokens ?? 0;
|
|
8461
|
+
totalCriticTokens += e.criticTokens ?? 0;
|
|
8462
|
+
// Track last synthesized
|
|
8463
|
+
if (e.lastSynthesized &&
|
|
8464
|
+
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
8465
|
+
lastSynthesizedAt = e.lastSynthesized;
|
|
8466
|
+
lastSynthesizedPath = e.path;
|
|
8377
8467
|
}
|
|
8378
|
-
|
|
8379
|
-
const
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8468
|
+
// Track stalest (effective staleness for scheduling)
|
|
8469
|
+
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
8470
|
+
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
8471
|
+
if (effectiveStaleness > stalestEffective) {
|
|
8472
|
+
stalestEffective = effectiveStaleness;
|
|
8473
|
+
stalestPath = e.path;
|
|
8383
8474
|
}
|
|
8384
8475
|
}
|
|
8476
|
+
return {
|
|
8477
|
+
total: entries.length,
|
|
8478
|
+
stale: staleCount,
|
|
8479
|
+
errors: errorCount,
|
|
8480
|
+
locked: lockedCount,
|
|
8481
|
+
disabled: disabledCount,
|
|
8482
|
+
neverSynthesized: neverSynthesizedCount,
|
|
8483
|
+
tokens: {
|
|
8484
|
+
architect: totalArchitectTokens,
|
|
8485
|
+
builder: totalBuilderTokens,
|
|
8486
|
+
critic: totalCriticTokens,
|
|
8487
|
+
},
|
|
8488
|
+
stalestPath,
|
|
8489
|
+
lastSynthesizedPath,
|
|
8490
|
+
lastSynthesizedAt,
|
|
8491
|
+
};
|
|
8385
8492
|
}
|
|
8386
8493
|
|
|
8387
8494
|
/**
|
|
8388
|
-
*
|
|
8495
|
+
* Normalize file paths to forward slashes for consistency with watcher-indexed paths.
|
|
8389
8496
|
*
|
|
8390
|
-
*
|
|
8497
|
+
* Watcher indexes paths with forward slashes (`j:/domains/...`). This utility
|
|
8498
|
+
* ensures all paths in the library use the same convention, regardless of
|
|
8499
|
+
* the platform's native separator.
|
|
8391
8500
|
*
|
|
8392
|
-
* @module
|
|
8501
|
+
* @module normalizePath
|
|
8393
8502
|
*/
|
|
8394
|
-
/**
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
architectEvery: z.number().int().min(1).default(10),
|
|
8404
|
-
/** Exponent for depth weighting in staleness formula. */
|
|
8405
|
-
depthWeight: z.number().min(0).default(0.5),
|
|
8406
|
-
/** Maximum archive snapshots to retain per meta. */
|
|
8407
|
-
maxArchive: z.number().int().min(1).default(20),
|
|
8408
|
-
/** Maximum lines of context to include in subprocess prompts. */
|
|
8409
|
-
maxLines: z.number().int().min(50).default(500),
|
|
8410
|
-
/** Architect subprocess timeout in seconds. */
|
|
8411
|
-
architectTimeout: z.number().int().min(30).default(180),
|
|
8412
|
-
/** Builder subprocess timeout in seconds. */
|
|
8413
|
-
builderTimeout: z.number().int().min(60).default(360),
|
|
8414
|
-
/** Critic subprocess timeout in seconds. */
|
|
8415
|
-
criticTimeout: z.number().int().min(30).default(240),
|
|
8416
|
-
/** Thinking level for spawned synthesis sessions. */
|
|
8417
|
-
thinking: z.string().default('low'),
|
|
8418
|
-
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
8419
|
-
defaultArchitect: z.string().optional(),
|
|
8420
|
-
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
8421
|
-
defaultCritic: z.string().optional(),
|
|
8422
|
-
/** Skip unchanged candidates, bump _generatedAt. */
|
|
8423
|
-
skipUnchanged: z.boolean().default(true),
|
|
8424
|
-
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
8425
|
-
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
8426
|
-
/** Watcher metadata properties applied to archive snapshots. */
|
|
8427
|
-
metaArchiveProperty: z
|
|
8428
|
-
.record(z.string(), z.unknown())
|
|
8429
|
-
.default({ _meta: 'archive' }),
|
|
8430
|
-
});
|
|
8431
|
-
/** Zod schema for logging configuration. */
|
|
8432
|
-
const loggingSchema = z.object({
|
|
8433
|
-
/** Log level. */
|
|
8434
|
-
level: z.string().default('info'),
|
|
8435
|
-
/** Optional file path for log output. */
|
|
8436
|
-
file: z.string().optional(),
|
|
8437
|
-
});
|
|
8438
|
-
/** Zod schema for a single auto-seed policy rule. */
|
|
8439
|
-
const autoSeedRuleSchema = z.object({
|
|
8440
|
-
/** Glob pattern matched against watcher walk results. */
|
|
8441
|
-
match: z.string(),
|
|
8442
|
-
/** Optional steering prompt for seeded metas. */
|
|
8443
|
-
steer: z.string().optional(),
|
|
8444
|
-
/** Optional cross-references for seeded metas. */
|
|
8445
|
-
crossRefs: z.array(z.string()).optional(),
|
|
8446
|
-
});
|
|
8447
|
-
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
8448
|
-
const serviceConfigSchema = metaConfigSchema.extend({
|
|
8449
|
-
/** HTTP port for the service (default: 1938). */
|
|
8450
|
-
port: z.number().int().min(1).max(65535).default(1938),
|
|
8451
|
-
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
8452
|
-
schedule: z.string().default('*/30 * * * *'),
|
|
8453
|
-
/** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
|
|
8454
|
-
reportChannel: z.string().optional(),
|
|
8455
|
-
/** Channel/user ID to send progress messages to. */
|
|
8456
|
-
reportTarget: z.string().optional(),
|
|
8457
|
-
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
8458
|
-
serverBaseUrl: z.string().optional(),
|
|
8459
|
-
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
8460
|
-
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
8461
|
-
/** Logging configuration. */
|
|
8462
|
-
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
8463
|
-
/**
|
|
8464
|
-
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
8465
|
-
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
8466
|
-
*/
|
|
8467
|
-
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
8468
|
-
});
|
|
8503
|
+
/**
|
|
8504
|
+
* Normalize a file path to forward slashes.
|
|
8505
|
+
*
|
|
8506
|
+
* @param p - File path (may contain backslashes).
|
|
8507
|
+
* @returns Path with all backslashes replaced by forward slashes.
|
|
8508
|
+
*/
|
|
8509
|
+
function normalizePath(p) {
|
|
8510
|
+
return p.replaceAll('\\', '/');
|
|
8511
|
+
}
|
|
8469
8512
|
|
|
8470
8513
|
/**
|
|
8471
|
-
*
|
|
8514
|
+
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
8472
8515
|
*
|
|
8473
|
-
*
|
|
8516
|
+
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
8517
|
+
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
8474
8518
|
*
|
|
8475
|
-
* @module
|
|
8519
|
+
* @module discovery/discoverMetas
|
|
8476
8520
|
*/
|
|
8477
8521
|
/**
|
|
8478
|
-
*
|
|
8479
|
-
*
|
|
8480
|
-
* @param value - Arbitrary JSON-compatible value.
|
|
8481
|
-
* @returns Value with env-var placeholders resolved.
|
|
8482
|
-
*/
|
|
8483
|
-
function substituteEnvVars(value) {
|
|
8484
|
-
if (typeof value === 'string') {
|
|
8485
|
-
return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
|
|
8486
|
-
const envVal = process.env[name];
|
|
8487
|
-
if (envVal === undefined) {
|
|
8488
|
-
throw new Error(`Environment variable ${name} is not set`);
|
|
8489
|
-
}
|
|
8490
|
-
return envVal;
|
|
8491
|
-
});
|
|
8492
|
-
}
|
|
8493
|
-
if (Array.isArray(value)) {
|
|
8494
|
-
return value.map(substituteEnvVars);
|
|
8495
|
-
}
|
|
8496
|
-
if (value !== null && typeof value === 'object') {
|
|
8497
|
-
const result = {};
|
|
8498
|
-
for (const [key, val] of Object.entries(value)) {
|
|
8499
|
-
result[key] = substituteEnvVars(val);
|
|
8500
|
-
}
|
|
8501
|
-
return result;
|
|
8502
|
-
}
|
|
8503
|
-
return value;
|
|
8504
|
-
}
|
|
8505
|
-
/**
|
|
8506
|
-
* Resolve \@file: references in a config value.
|
|
8507
|
-
*
|
|
8508
|
-
* @param value - String value that may start with "\@file:".
|
|
8509
|
-
* @param baseDir - Base directory for resolving relative paths.
|
|
8510
|
-
* @returns The resolved string (file contents or original value).
|
|
8511
|
-
*/
|
|
8512
|
-
function resolveFileRef(value, baseDir) {
|
|
8513
|
-
if (!value.startsWith('@file:'))
|
|
8514
|
-
return value;
|
|
8515
|
-
const filePath = join(baseDir, value.slice(6));
|
|
8516
|
-
return readFileSync(filePath, 'utf8');
|
|
8517
|
-
}
|
|
8518
|
-
/**
|
|
8519
|
-
* Load service config from a JSON file.
|
|
8520
|
-
*
|
|
8521
|
-
* Resolves \@file: references for defaultArchitect and defaultCritic,
|
|
8522
|
-
* and substitutes environment-variable placeholders throughout.
|
|
8523
|
-
*
|
|
8524
|
-
* @param configPath - Path to config JSON file.
|
|
8525
|
-
* @returns Validated ServiceConfig.
|
|
8526
|
-
*/
|
|
8527
|
-
function loadServiceConfig(configPath) {
|
|
8528
|
-
const rawText = readFileSync(configPath, 'utf8');
|
|
8529
|
-
const raw = substituteEnvVars(JSON.parse(rawText));
|
|
8530
|
-
const baseDir = dirname(configPath);
|
|
8531
|
-
if (typeof raw['defaultArchitect'] === 'string') {
|
|
8532
|
-
raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
|
|
8533
|
-
}
|
|
8534
|
-
if (typeof raw['defaultCritic'] === 'string') {
|
|
8535
|
-
raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
|
|
8536
|
-
}
|
|
8537
|
-
return serviceConfigSchema.parse(raw);
|
|
8538
|
-
}
|
|
8539
|
-
|
|
8540
|
-
/**
|
|
8541
|
-
* Compute summary statistics from an array of MetaEntry objects.
|
|
8542
|
-
*
|
|
8543
|
-
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
8544
|
-
*
|
|
8545
|
-
* @module discovery/computeSummary
|
|
8546
|
-
*/
|
|
8547
|
-
/**
|
|
8548
|
-
* Compute summary statistics from a list of meta entries.
|
|
8549
|
-
*
|
|
8550
|
-
* @param entries - Enriched meta entries (full or filtered).
|
|
8551
|
-
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
8552
|
-
* @returns Aggregated summary statistics.
|
|
8553
|
-
*/
|
|
8554
|
-
function computeSummary(entries, depthWeight) {
|
|
8555
|
-
let staleCount = 0;
|
|
8556
|
-
let errorCount = 0;
|
|
8557
|
-
let lockedCount = 0;
|
|
8558
|
-
let disabledCount = 0;
|
|
8559
|
-
let neverSynthesizedCount = 0;
|
|
8560
|
-
let totalArchitectTokens = 0;
|
|
8561
|
-
let totalBuilderTokens = 0;
|
|
8562
|
-
let totalCriticTokens = 0;
|
|
8563
|
-
let stalestPath = null;
|
|
8564
|
-
let stalestEffective = -1;
|
|
8565
|
-
let lastSynthesizedPath = null;
|
|
8566
|
-
let lastSynthesizedAt = null;
|
|
8567
|
-
for (const e of entries) {
|
|
8568
|
-
if (e.stalenessSeconds > 0)
|
|
8569
|
-
staleCount++;
|
|
8570
|
-
if (e.hasError)
|
|
8571
|
-
errorCount++;
|
|
8572
|
-
if (e.locked)
|
|
8573
|
-
lockedCount++;
|
|
8574
|
-
if (e.disabled)
|
|
8575
|
-
disabledCount++;
|
|
8576
|
-
if (e.lastSynthesized === null)
|
|
8577
|
-
neverSynthesizedCount++;
|
|
8578
|
-
totalArchitectTokens += e.architectTokens ?? 0;
|
|
8579
|
-
totalBuilderTokens += e.builderTokens ?? 0;
|
|
8580
|
-
totalCriticTokens += e.criticTokens ?? 0;
|
|
8581
|
-
// Track last synthesized
|
|
8582
|
-
if (e.lastSynthesized &&
|
|
8583
|
-
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
8584
|
-
lastSynthesizedAt = e.lastSynthesized;
|
|
8585
|
-
lastSynthesizedPath = e.path;
|
|
8586
|
-
}
|
|
8587
|
-
// Track stalest (effective staleness for scheduling)
|
|
8588
|
-
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
8589
|
-
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
8590
|
-
if (effectiveStaleness > stalestEffective) {
|
|
8591
|
-
stalestEffective = effectiveStaleness;
|
|
8592
|
-
stalestPath = e.path;
|
|
8593
|
-
}
|
|
8594
|
-
}
|
|
8595
|
-
return {
|
|
8596
|
-
total: entries.length,
|
|
8597
|
-
stale: staleCount,
|
|
8598
|
-
errors: errorCount,
|
|
8599
|
-
locked: lockedCount,
|
|
8600
|
-
disabled: disabledCount,
|
|
8601
|
-
neverSynthesized: neverSynthesizedCount,
|
|
8602
|
-
tokens: {
|
|
8603
|
-
architect: totalArchitectTokens,
|
|
8604
|
-
builder: totalBuilderTokens,
|
|
8605
|
-
critic: totalCriticTokens,
|
|
8606
|
-
},
|
|
8607
|
-
stalestPath,
|
|
8608
|
-
lastSynthesizedPath,
|
|
8609
|
-
lastSynthesizedAt,
|
|
8610
|
-
};
|
|
8611
|
-
}
|
|
8612
|
-
|
|
8613
|
-
/**
|
|
8614
|
-
* Normalize file paths to forward slashes for consistency with watcher-indexed paths.
|
|
8615
|
-
*
|
|
8616
|
-
* Watcher indexes paths with forward slashes (`j:/domains/...`). This utility
|
|
8617
|
-
* ensures all paths in the library use the same convention, regardless of
|
|
8618
|
-
* the platform's native separator.
|
|
8619
|
-
*
|
|
8620
|
-
* @module normalizePath
|
|
8621
|
-
*/
|
|
8622
|
-
/**
|
|
8623
|
-
* Normalize a file path to forward slashes.
|
|
8624
|
-
*
|
|
8625
|
-
* @param p - File path (may contain backslashes).
|
|
8626
|
-
* @returns Path with all backslashes replaced by forward slashes.
|
|
8627
|
-
*/
|
|
8628
|
-
function normalizePath(p) {
|
|
8629
|
-
return p.replaceAll('\\', '/');
|
|
8630
|
-
}
|
|
8631
|
-
|
|
8632
|
-
/**
|
|
8633
|
-
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
8634
|
-
*
|
|
8635
|
-
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
8636
|
-
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
8637
|
-
*
|
|
8638
|
-
* @module discovery/discoverMetas
|
|
8639
|
-
*/
|
|
8640
|
-
/**
|
|
8641
|
-
* Discover all .meta/ directories via watcher walk.
|
|
8522
|
+
* Discover all .meta/ directories via watcher walk.
|
|
8642
8523
|
*
|
|
8643
8524
|
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
8644
8525
|
* and returns deduplicated meta directory paths.
|
|
@@ -9193,6 +9074,263 @@ function getDeltaFiles(generatedAt, scopeFiles) {
|
|
|
9193
9074
|
return filterModifiedAfter(scopeFiles, new Date(generatedAt).getTime());
|
|
9194
9075
|
}
|
|
9195
9076
|
|
|
9077
|
+
/**
|
|
9078
|
+
* In-memory cache for listMetas results with TTL and concurrent refresh guard.
|
|
9079
|
+
*
|
|
9080
|
+
* @module cache
|
|
9081
|
+
*/
|
|
9082
|
+
const TTL_MS = 60_000;
|
|
9083
|
+
/**
|
|
9084
|
+
* Caches listMetas results to avoid expensive repeated filesystem walks.
|
|
9085
|
+
* Supports concurrent refresh coalescing and manual invalidation.
|
|
9086
|
+
*/
|
|
9087
|
+
class MetaCache {
|
|
9088
|
+
result = null;
|
|
9089
|
+
updatedAt = 0;
|
|
9090
|
+
refreshPromise = null;
|
|
9091
|
+
/** Get cached result or refresh if stale. */
|
|
9092
|
+
async get(config, watcher) {
|
|
9093
|
+
if (this.result && Date.now() - this.updatedAt < TTL_MS) {
|
|
9094
|
+
return this.result;
|
|
9095
|
+
}
|
|
9096
|
+
return this.refresh(config, watcher);
|
|
9097
|
+
}
|
|
9098
|
+
/** Force-expire the cache so next get() triggers a refresh. */
|
|
9099
|
+
invalidate() {
|
|
9100
|
+
this.updatedAt = 0;
|
|
9101
|
+
}
|
|
9102
|
+
async refresh(config, watcher) {
|
|
9103
|
+
if (this.refreshPromise)
|
|
9104
|
+
return this.refreshPromise;
|
|
9105
|
+
this.refreshPromise = listMetas(config, watcher)
|
|
9106
|
+
.then((result) => {
|
|
9107
|
+
this.result = result;
|
|
9108
|
+
this.updatedAt = Date.now();
|
|
9109
|
+
return result;
|
|
9110
|
+
})
|
|
9111
|
+
.finally(() => {
|
|
9112
|
+
this.refreshPromise = null;
|
|
9113
|
+
});
|
|
9114
|
+
return this.refreshPromise;
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
|
|
9118
|
+
/**
|
|
9119
|
+
* Shared live config hot-reload support.
|
|
9120
|
+
*
|
|
9121
|
+
* Used by both file-watch reloads in bootstrap and POST /config/apply
|
|
9122
|
+
* via the component descriptor's onConfigApply callback.
|
|
9123
|
+
*
|
|
9124
|
+
* @module configHotReload
|
|
9125
|
+
*/
|
|
9126
|
+
/**
|
|
9127
|
+
* Fields that require a service restart to take effect.
|
|
9128
|
+
*
|
|
9129
|
+
* Shared between the descriptor's `onConfigApply` and the file-watcher
|
|
9130
|
+
* hot-reload in `bootstrap.ts`.
|
|
9131
|
+
*/
|
|
9132
|
+
const RESTART_REQUIRED_FIELDS = [
|
|
9133
|
+
'port',
|
|
9134
|
+
'watcherUrl',
|
|
9135
|
+
'gatewayUrl',
|
|
9136
|
+
'gatewayApiKey',
|
|
9137
|
+
'defaultArchitect',
|
|
9138
|
+
'defaultCritic',
|
|
9139
|
+
];
|
|
9140
|
+
let runtime = null;
|
|
9141
|
+
/** Register the active service runtime for config-apply hot reload. */
|
|
9142
|
+
function registerConfigHotReloadRuntime(nextRuntime) {
|
|
9143
|
+
runtime = nextRuntime;
|
|
9144
|
+
}
|
|
9145
|
+
/** Apply hot-reloadable config changes to the live shared config object. */
|
|
9146
|
+
function applyHotReloadedConfig(newConfig) {
|
|
9147
|
+
if (!runtime)
|
|
9148
|
+
return;
|
|
9149
|
+
const { config, logger, scheduler } = runtime;
|
|
9150
|
+
for (const field of RESTART_REQUIRED_FIELDS) {
|
|
9151
|
+
const oldVal = config[field];
|
|
9152
|
+
const nextVal = newConfig[field];
|
|
9153
|
+
if (oldVal !== nextVal) {
|
|
9154
|
+
logger.warn({ field, oldValue: oldVal, newValue: nextVal }, 'Config field changed but requires restart to take effect');
|
|
9155
|
+
}
|
|
9156
|
+
}
|
|
9157
|
+
if (newConfig.schedule !== config.schedule) {
|
|
9158
|
+
scheduler?.updateSchedule(newConfig.schedule);
|
|
9159
|
+
config.schedule = newConfig.schedule;
|
|
9160
|
+
logger.info({ schedule: newConfig.schedule }, 'Schedule hot-reloaded');
|
|
9161
|
+
}
|
|
9162
|
+
if (newConfig.logging.level !== config.logging.level) {
|
|
9163
|
+
logger.level = newConfig.logging.level;
|
|
9164
|
+
config.logging.level = newConfig.logging.level;
|
|
9165
|
+
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
9166
|
+
}
|
|
9167
|
+
const restartSet = new Set(RESTART_REQUIRED_FIELDS);
|
|
9168
|
+
for (const key of Object.keys(newConfig)) {
|
|
9169
|
+
if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
|
|
9170
|
+
continue;
|
|
9171
|
+
}
|
|
9172
|
+
const oldVal = config[key];
|
|
9173
|
+
const nextVal = newConfig[key];
|
|
9174
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(nextVal)) {
|
|
9175
|
+
config[key] = nextVal;
|
|
9176
|
+
logger.info({ field: key }, 'Config field hot-reloaded');
|
|
9177
|
+
}
|
|
9178
|
+
}
|
|
9179
|
+
}
|
|
9180
|
+
|
|
9181
|
+
/**
|
|
9182
|
+
* Zod schema for jeeves-meta service configuration.
|
|
9183
|
+
*
|
|
9184
|
+
* The service config is a strict superset of the core (library-compatible) meta config.
|
|
9185
|
+
*
|
|
9186
|
+
* @module schema/config
|
|
9187
|
+
*/
|
|
9188
|
+
/** Zod schema for the core (library-compatible) meta configuration. */
|
|
9189
|
+
const metaConfigSchema = z.object({
|
|
9190
|
+
/** Watcher service base URL. */
|
|
9191
|
+
watcherUrl: z.url(),
|
|
9192
|
+
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
9193
|
+
gatewayUrl: z.url().default('http://127.0.0.1:18789'),
|
|
9194
|
+
/** Optional API key for gateway authentication. */
|
|
9195
|
+
gatewayApiKey: z.string().optional(),
|
|
9196
|
+
/** Run architect every N cycles (per meta). */
|
|
9197
|
+
architectEvery: z.number().int().min(1).default(10),
|
|
9198
|
+
/** Exponent for depth weighting in staleness formula. */
|
|
9199
|
+
depthWeight: z.number().min(0).default(0.5),
|
|
9200
|
+
/** Maximum archive snapshots to retain per meta. */
|
|
9201
|
+
maxArchive: z.number().int().min(1).default(20),
|
|
9202
|
+
/** Maximum lines of context to include in subprocess prompts. */
|
|
9203
|
+
maxLines: z.number().int().min(50).default(500),
|
|
9204
|
+
/** Architect subprocess timeout in seconds. */
|
|
9205
|
+
architectTimeout: z.number().int().min(30).default(180),
|
|
9206
|
+
/** Builder subprocess timeout in seconds. */
|
|
9207
|
+
builderTimeout: z.number().int().min(60).default(360),
|
|
9208
|
+
/** Critic subprocess timeout in seconds. */
|
|
9209
|
+
criticTimeout: z.number().int().min(30).default(240),
|
|
9210
|
+
/** Thinking level for spawned synthesis sessions. */
|
|
9211
|
+
thinking: z.string().default('low'),
|
|
9212
|
+
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
9213
|
+
defaultArchitect: z.string().optional(),
|
|
9214
|
+
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
9215
|
+
defaultCritic: z.string().optional(),
|
|
9216
|
+
/** Skip unchanged candidates, bump _generatedAt. */
|
|
9217
|
+
skipUnchanged: z.boolean().default(true),
|
|
9218
|
+
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
9219
|
+
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
9220
|
+
/** Watcher metadata properties applied to archive snapshots. */
|
|
9221
|
+
metaArchiveProperty: z
|
|
9222
|
+
.record(z.string(), z.unknown())
|
|
9223
|
+
.default({ _meta: 'archive' }),
|
|
9224
|
+
});
|
|
9225
|
+
/** Zod schema for logging configuration. */
|
|
9226
|
+
const loggingSchema = z.object({
|
|
9227
|
+
/** Log level. */
|
|
9228
|
+
level: z.string().default('info'),
|
|
9229
|
+
/** Optional file path for log output. */
|
|
9230
|
+
file: z.string().optional(),
|
|
9231
|
+
});
|
|
9232
|
+
/** Zod schema for a single auto-seed policy rule. */
|
|
9233
|
+
const autoSeedRuleSchema = z.object({
|
|
9234
|
+
/** Glob pattern matched against watcher walk results. */
|
|
9235
|
+
match: z.string(),
|
|
9236
|
+
/** Optional steering prompt for seeded metas. */
|
|
9237
|
+
steer: z.string().optional(),
|
|
9238
|
+
/** Optional cross-references for seeded metas. */
|
|
9239
|
+
crossRefs: z.array(z.string()).optional(),
|
|
9240
|
+
});
|
|
9241
|
+
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
9242
|
+
const serviceConfigSchema = metaConfigSchema.extend({
|
|
9243
|
+
/** HTTP port for the service (default: 1938). */
|
|
9244
|
+
port: z.number().int().min(1).max(65535).default(1938),
|
|
9245
|
+
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
9246
|
+
schedule: z.string().default('*/30 * * * *'),
|
|
9247
|
+
/** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
|
|
9248
|
+
reportChannel: z.string().optional(),
|
|
9249
|
+
/** Channel/user ID to send progress messages to. */
|
|
9250
|
+
reportTarget: z.string().optional(),
|
|
9251
|
+
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
9252
|
+
serverBaseUrl: z.string().optional(),
|
|
9253
|
+
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
9254
|
+
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
9255
|
+
/** Logging configuration. */
|
|
9256
|
+
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
9257
|
+
/**
|
|
9258
|
+
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
9259
|
+
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
9260
|
+
*/
|
|
9261
|
+
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
9262
|
+
});
|
|
9263
|
+
|
|
9264
|
+
/**
|
|
9265
|
+
* Load and resolve jeeves-meta service config.
|
|
9266
|
+
*
|
|
9267
|
+
* Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
|
|
9268
|
+
*
|
|
9269
|
+
* @module configLoader
|
|
9270
|
+
*/
|
|
9271
|
+
/**
|
|
9272
|
+
* Deep-walk a value, replacing `\${VAR\}` patterns with process.env values.
|
|
9273
|
+
*
|
|
9274
|
+
* @param value - Arbitrary JSON-compatible value.
|
|
9275
|
+
* @returns Value with env-var placeholders resolved.
|
|
9276
|
+
*/
|
|
9277
|
+
function substituteEnvVars(value) {
|
|
9278
|
+
if (typeof value === 'string') {
|
|
9279
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
|
|
9280
|
+
const envVal = process.env[name];
|
|
9281
|
+
if (envVal === undefined) {
|
|
9282
|
+
throw new Error(`Environment variable ${name} is not set`);
|
|
9283
|
+
}
|
|
9284
|
+
return envVal;
|
|
9285
|
+
});
|
|
9286
|
+
}
|
|
9287
|
+
if (Array.isArray(value)) {
|
|
9288
|
+
return value.map(substituteEnvVars);
|
|
9289
|
+
}
|
|
9290
|
+
if (value !== null && typeof value === 'object') {
|
|
9291
|
+
const result = {};
|
|
9292
|
+
for (const [key, val] of Object.entries(value)) {
|
|
9293
|
+
result[key] = substituteEnvVars(val);
|
|
9294
|
+
}
|
|
9295
|
+
return result;
|
|
9296
|
+
}
|
|
9297
|
+
return value;
|
|
9298
|
+
}
|
|
9299
|
+
/**
|
|
9300
|
+
* Resolve \@file: references in a config value.
|
|
9301
|
+
*
|
|
9302
|
+
* @param value - String value that may start with "\@file:".
|
|
9303
|
+
* @param baseDir - Base directory for resolving relative paths.
|
|
9304
|
+
* @returns The resolved string (file contents or original value).
|
|
9305
|
+
*/
|
|
9306
|
+
function resolveFileRef(value, baseDir) {
|
|
9307
|
+
if (!value.startsWith('@file:'))
|
|
9308
|
+
return value;
|
|
9309
|
+
const filePath = join(baseDir, value.slice(6));
|
|
9310
|
+
return readFileSync(filePath, 'utf8');
|
|
9311
|
+
}
|
|
9312
|
+
/**
|
|
9313
|
+
* Load service config from a JSON file.
|
|
9314
|
+
*
|
|
9315
|
+
* Resolves \@file: references for defaultArchitect and defaultCritic,
|
|
9316
|
+
* and substitutes environment-variable placeholders throughout.
|
|
9317
|
+
*
|
|
9318
|
+
* @param configPath - Path to config JSON file.
|
|
9319
|
+
* @returns Validated ServiceConfig.
|
|
9320
|
+
*/
|
|
9321
|
+
function loadServiceConfig(configPath) {
|
|
9322
|
+
const rawText = readFileSync(configPath, 'utf8');
|
|
9323
|
+
const raw = substituteEnvVars(JSON.parse(rawText));
|
|
9324
|
+
const baseDir = dirname(configPath);
|
|
9325
|
+
if (typeof raw['defaultArchitect'] === 'string') {
|
|
9326
|
+
raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
|
|
9327
|
+
}
|
|
9328
|
+
if (typeof raw['defaultCritic'] === 'string') {
|
|
9329
|
+
raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
|
|
9330
|
+
}
|
|
9331
|
+
return serviceConfigSchema.parse(raw);
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9196
9334
|
/**
|
|
9197
9335
|
* Error thrown when a spawned subprocess is aborted via AbortController.
|
|
9198
9336
|
*
|
|
@@ -9291,21 +9429,29 @@ class GatewayExecutor {
|
|
|
9291
9429
|
}
|
|
9292
9430
|
return data;
|
|
9293
9431
|
}
|
|
9294
|
-
/** Look up
|
|
9295
|
-
async
|
|
9432
|
+
/** Look up session metadata (tokens, completion status) via sessions_list. */
|
|
9433
|
+
async getSessionInfo(sessionKey) {
|
|
9296
9434
|
try {
|
|
9297
9435
|
const result = await this.invoke('sessions_list', {
|
|
9298
|
-
limit:
|
|
9436
|
+
limit: 200,
|
|
9299
9437
|
messageLimit: 0,
|
|
9300
9438
|
});
|
|
9301
9439
|
const sessions = (result.result?.details?.sessions ??
|
|
9302
9440
|
result.result?.sessions ??
|
|
9303
9441
|
[]);
|
|
9304
9442
|
const match = sessions.find((s) => s.key === sessionKey);
|
|
9305
|
-
|
|
9443
|
+
if (!match) {
|
|
9444
|
+
// Session absent from list — likely cleaned up after completion.
|
|
9445
|
+
// With limit=200 this is reliable; a false positive here only
|
|
9446
|
+
// means we read the output file slightly early (still correct
|
|
9447
|
+
// if the file exists).
|
|
9448
|
+
return { completed: true };
|
|
9449
|
+
}
|
|
9450
|
+
const done = match.status === 'completed' || match.status === 'done';
|
|
9451
|
+
return { tokens: match.totalTokens, completed: done };
|
|
9306
9452
|
}
|
|
9307
9453
|
catch {
|
|
9308
|
-
return
|
|
9454
|
+
return { completed: false };
|
|
9309
9455
|
}
|
|
9310
9456
|
}
|
|
9311
9457
|
/** Whether this executor has been aborted by the operator. */
|
|
@@ -9335,7 +9481,7 @@ class GatewayExecutor {
|
|
|
9335
9481
|
'Write your complete output to a file using the Write tool at:\n' +
|
|
9336
9482
|
outputPath +
|
|
9337
9483
|
'\n\n' +
|
|
9338
|
-
'
|
|
9484
|
+
'After writing the file, reply with ONLY: NO_REPLY';
|
|
9339
9485
|
// Step 1: Spawn the sub-agent session (unique label per cycle to avoid
|
|
9340
9486
|
// "label already in use" errors — gateway labels persist after session completion)
|
|
9341
9487
|
const labelBase = options?.label ?? 'jeeves-meta-synthesis';
|
|
@@ -9371,48 +9517,53 @@ class GatewayExecutor {
|
|
|
9371
9517
|
historyResult.result?.messages ??
|
|
9372
9518
|
[];
|
|
9373
9519
|
const msgArray = messages;
|
|
9520
|
+
// Check 1: terminal stop reason in history
|
|
9521
|
+
let historyDone = false;
|
|
9374
9522
|
if (msgArray.length > 0) {
|
|
9375
9523
|
const lastMsg = msgArray[msgArray.length - 1];
|
|
9376
|
-
// Complete when last message is assistant with a terminal stop reason
|
|
9377
9524
|
if (lastMsg.role === 'assistant' &&
|
|
9378
9525
|
lastMsg.stopReason &&
|
|
9379
9526
|
lastMsg.stopReason !== 'toolUse' &&
|
|
9380
9527
|
lastMsg.stopReason !== 'error') {
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9528
|
+
historyDone = true;
|
|
9529
|
+
}
|
|
9530
|
+
}
|
|
9531
|
+
// Check 2: session completion status via sessions_list
|
|
9532
|
+
const sessionInfo = await this.getSessionInfo(sessionKey);
|
|
9533
|
+
if (historyDone || sessionInfo.completed) {
|
|
9534
|
+
const tokens = sessionInfo.tokens;
|
|
9535
|
+
// Read output from file (sub-agent wrote it via Write tool)
|
|
9536
|
+
if (existsSync(outputPath)) {
|
|
9537
|
+
try {
|
|
9538
|
+
const output = readFileSync(outputPath, 'utf8');
|
|
9539
|
+
return { output, tokens };
|
|
9540
|
+
}
|
|
9541
|
+
finally {
|
|
9385
9542
|
try {
|
|
9386
|
-
|
|
9387
|
-
return { output, tokens };
|
|
9543
|
+
unlinkSync(outputPath);
|
|
9388
9544
|
}
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
unlinkSync(outputPath);
|
|
9392
|
-
}
|
|
9393
|
-
catch {
|
|
9394
|
-
/* cleanup best-effort */
|
|
9395
|
-
}
|
|
9545
|
+
catch {
|
|
9546
|
+
/* cleanup best-effort */
|
|
9396
9547
|
}
|
|
9397
9548
|
}
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9549
|
+
}
|
|
9550
|
+
// Fallback: extract from message content if file wasn't written
|
|
9551
|
+
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9552
|
+
const msg = msgArray[i];
|
|
9553
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
9554
|
+
const text = typeof msg.content === 'string'
|
|
9555
|
+
? msg.content
|
|
9556
|
+
: Array.isArray(msg.content)
|
|
9403
9557
|
? msg.content
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
if (text)
|
|
9411
|
-
return { output: text, tokens };
|
|
9412
|
-
}
|
|
9558
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
9559
|
+
.map((b) => b.text)
|
|
9560
|
+
.join('\n')
|
|
9561
|
+
: '';
|
|
9562
|
+
if (text)
|
|
9563
|
+
return { output: text, tokens };
|
|
9413
9564
|
}
|
|
9414
|
-
return { output: '', tokens };
|
|
9415
9565
|
}
|
|
9566
|
+
return { output: '', tokens };
|
|
9416
9567
|
}
|
|
9417
9568
|
}
|
|
9418
9569
|
catch {
|
|
@@ -9775,6 +9926,7 @@ async function buildContextPackage(node, meta, watcher, logger) {
|
|
|
9775
9926
|
*
|
|
9776
9927
|
* @module orchestrator/buildTask
|
|
9777
9928
|
*/
|
|
9929
|
+
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
9778
9930
|
/** Build the template context from synthesis inputs. */
|
|
9779
9931
|
function buildTemplateContext(ctx, meta, config) {
|
|
9780
9932
|
return {
|
|
@@ -9929,134 +10081,6 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
9929
10081
|
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
9930
10082
|
}
|
|
9931
10083
|
|
|
9932
|
-
/**
|
|
9933
|
-
* Structured error from a synthesis step failure.
|
|
9934
|
-
*
|
|
9935
|
-
* @module schema/error
|
|
9936
|
-
*/
|
|
9937
|
-
/** Zod schema for synthesis step errors. */
|
|
9938
|
-
const metaErrorSchema = z.object({
|
|
9939
|
-
/** Which step failed: 'architect', 'builder', or 'critic'. */
|
|
9940
|
-
step: z.enum(['architect', 'builder', 'critic']),
|
|
9941
|
-
/** Error classification code. */
|
|
9942
|
-
code: z.string(),
|
|
9943
|
-
/** Human-readable error message. */
|
|
9944
|
-
message: z.string(),
|
|
9945
|
-
});
|
|
9946
|
-
|
|
9947
|
-
/**
|
|
9948
|
-
* Zod schema for .meta/meta.json files.
|
|
9949
|
-
*
|
|
9950
|
-
* Reserved properties are underscore-prefixed and engine-managed.
|
|
9951
|
-
* All other keys are open schema (builder output).
|
|
9952
|
-
*
|
|
9953
|
-
* @module schema/meta
|
|
9954
|
-
*/
|
|
9955
|
-
/** Valid states for a synthesis phase. */
|
|
9956
|
-
const phaseStatuses = [
|
|
9957
|
-
'fresh',
|
|
9958
|
-
'stale',
|
|
9959
|
-
'pending',
|
|
9960
|
-
'running',
|
|
9961
|
-
'failed',
|
|
9962
|
-
];
|
|
9963
|
-
/** Zod schema for a per-phase status value. */
|
|
9964
|
-
const phaseStatusSchema = z.enum(phaseStatuses);
|
|
9965
|
-
/** Zod schema for the per-meta phase state record. */
|
|
9966
|
-
const phaseStateSchema = z.object({
|
|
9967
|
-
architect: phaseStatusSchema,
|
|
9968
|
-
builder: phaseStatusSchema,
|
|
9969
|
-
critic: phaseStatusSchema,
|
|
9970
|
-
});
|
|
9971
|
-
/** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
|
|
9972
|
-
z
|
|
9973
|
-
.object({
|
|
9974
|
-
/** Stable identity. Auto-generated on first synthesis if not provided. */
|
|
9975
|
-
_id: z.uuid().optional(),
|
|
9976
|
-
/** Human-provided steering prompt. Optional. */
|
|
9977
|
-
_steer: z.string().optional(),
|
|
9978
|
-
/**
|
|
9979
|
-
* Explicit cross-references to other meta owner paths.
|
|
9980
|
-
* Referenced metas' _content is included as architect/builder context.
|
|
9981
|
-
*/
|
|
9982
|
-
_crossRefs: z.array(z.string()).optional(),
|
|
9983
|
-
/** Architect system prompt used this turn. Defaults from config. */
|
|
9984
|
-
_architect: z.string().optional(),
|
|
9985
|
-
/**
|
|
9986
|
-
* Task brief generated by the architect. Cached and reused across cycles;
|
|
9987
|
-
* regenerated only when triggered.
|
|
9988
|
-
*/
|
|
9989
|
-
_builder: z.string().optional(),
|
|
9990
|
-
/** Critic system prompt used this turn. Defaults from config. */
|
|
9991
|
-
_critic: z.string().optional(),
|
|
9992
|
-
/** Timestamp of last synthesis. ISO 8601. */
|
|
9993
|
-
_generatedAt: z.iso.datetime().optional(),
|
|
9994
|
-
/** Narrative synthesis output. Rendered by watcher for embedding. */
|
|
9995
|
-
_content: z.string().optional(),
|
|
9996
|
-
/**
|
|
9997
|
-
* Hash of sorted file listing in scope. Detects directory structure
|
|
9998
|
-
* changes that trigger an architect re-run.
|
|
9999
|
-
*/
|
|
10000
|
-
_structureHash: z.string().optional(),
|
|
10001
|
-
/**
|
|
10002
|
-
* Cycles since last architect run. Reset to 0 when architect runs.
|
|
10003
|
-
* Used with architectEvery to trigger periodic re-prompting.
|
|
10004
|
-
*/
|
|
10005
|
-
_synthesisCount: z.number().int().min(0).optional(),
|
|
10006
|
-
/** Critic evaluation of the last synthesis. */
|
|
10007
|
-
_feedback: z.string().optional(),
|
|
10008
|
-
/**
|
|
10009
|
-
* Present and true on archive snapshots. Distinguishes live vs. archived
|
|
10010
|
-
* metas.
|
|
10011
|
-
*/
|
|
10012
|
-
_archived: z.boolean().optional(),
|
|
10013
|
-
/** Timestamp when this snapshot was archived. ISO 8601. */
|
|
10014
|
-
_archivedAt: z.iso.datetime().optional(),
|
|
10015
|
-
/**
|
|
10016
|
-
* Scheduling priority. Higher = updates more often. Negative allowed;
|
|
10017
|
-
* normalized to min 0 at scheduling time.
|
|
10018
|
-
*/
|
|
10019
|
-
_depth: z.number().optional(),
|
|
10020
|
-
/**
|
|
10021
|
-
* Emphasis multiplier for depth weighting in scheduling.
|
|
10022
|
-
* Default 1. Higher values increase this meta's scheduling priority
|
|
10023
|
-
* relative to its depth. Set to 0.5 to halve the depth effect,
|
|
10024
|
-
* 2 to double it, 0 to ignore depth entirely for this meta.
|
|
10025
|
-
*/
|
|
10026
|
-
_emphasis: z.number().min(0).optional(),
|
|
10027
|
-
/** Token count from last architect subprocess call. */
|
|
10028
|
-
_architectTokens: z.number().int().optional(),
|
|
10029
|
-
/** Token count from last builder subprocess call. */
|
|
10030
|
-
_builderTokens: z.number().int().optional(),
|
|
10031
|
-
/** Token count from last critic subprocess call. */
|
|
10032
|
-
_criticTokens: z.number().int().optional(),
|
|
10033
|
-
/** Exponential moving average of architect token usage (decay 0.3). */
|
|
10034
|
-
_architectTokensAvg: z.number().optional(),
|
|
10035
|
-
/** Exponential moving average of builder token usage (decay 0.3). */
|
|
10036
|
-
_builderTokensAvg: z.number().optional(),
|
|
10037
|
-
/** Exponential moving average of critic token usage (decay 0.3). */
|
|
10038
|
-
_criticTokensAvg: z.number().optional(),
|
|
10039
|
-
/**
|
|
10040
|
-
* Opaque state carried across synthesis cycles for progressive work.
|
|
10041
|
-
* Set by the builder, passed back as context on next cycle.
|
|
10042
|
-
*/
|
|
10043
|
-
_state: z.unknown().optional(),
|
|
10044
|
-
/**
|
|
10045
|
-
* Structured error from last cycle. Present when a step failed.
|
|
10046
|
-
* Cleared on successful cycle.
|
|
10047
|
-
*/
|
|
10048
|
-
_error: metaErrorSchema.optional(),
|
|
10049
|
-
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
10050
|
-
_disabled: z.boolean().optional(),
|
|
10051
|
-
/**
|
|
10052
|
-
* Per-phase state machine record. Engine-managed.
|
|
10053
|
-
* Keyed by phase name (architect, builder, critic) with status values.
|
|
10054
|
-
* Persisted to survive ticks; derived on first load for back-compat.
|
|
10055
|
-
*/
|
|
10056
|
-
_phaseState: phaseStateSchema.optional(),
|
|
10057
|
-
})
|
|
10058
|
-
.loose();
|
|
10059
|
-
|
|
10060
10084
|
/**
|
|
10061
10085
|
* Build a minimal MetaNode from a known meta path using watcher walk.
|
|
10062
10086
|
*
|
|
@@ -10118,222 +10142,6 @@ async function buildMinimalNode(metaPath, watcher) {
|
|
|
10118
10142
|
return node;
|
|
10119
10143
|
}
|
|
10120
10144
|
|
|
10121
|
-
/**
|
|
10122
|
-
* Weighted staleness formula for candidate selection.
|
|
10123
|
-
*
|
|
10124
|
-
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10125
|
-
*
|
|
10126
|
-
* @module scheduling/weightedFormula
|
|
10127
|
-
*/
|
|
10128
|
-
/**
|
|
10129
|
-
* Compute effective staleness for a set of candidates.
|
|
10130
|
-
*
|
|
10131
|
-
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
10132
|
-
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10133
|
-
*
|
|
10134
|
-
* Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
|
|
10135
|
-
* metas to tune how much their tree position affects scheduling.
|
|
10136
|
-
*
|
|
10137
|
-
* @param candidates - Array of \{ node, meta, actualStaleness \}.
|
|
10138
|
-
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
10139
|
-
* @returns Same array with effectiveStaleness computed.
|
|
10140
|
-
*/
|
|
10141
|
-
function computeEffectiveStaleness(candidates, depthWeight) {
|
|
10142
|
-
if (candidates.length === 0)
|
|
10143
|
-
return [];
|
|
10144
|
-
// Get depth for each candidate: use _depth override or tree depth
|
|
10145
|
-
const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
|
|
10146
|
-
// Normalize: shift so minimum becomes 0
|
|
10147
|
-
const minDepth = Math.min(...depths);
|
|
10148
|
-
const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
|
|
10149
|
-
return candidates.map((c, i) => {
|
|
10150
|
-
const emphasis = c.meta._emphasis ?? 1;
|
|
10151
|
-
return {
|
|
10152
|
-
...c,
|
|
10153
|
-
effectiveStaleness: c.actualStaleness *
|
|
10154
|
-
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
10155
|
-
};
|
|
10156
|
-
});
|
|
10157
|
-
}
|
|
10158
|
-
|
|
10159
|
-
/**
|
|
10160
|
-
* Select the best synthesis candidate from stale metas.
|
|
10161
|
-
*
|
|
10162
|
-
* Picks the meta with highest effective staleness.
|
|
10163
|
-
*
|
|
10164
|
-
* @module scheduling/selectCandidate
|
|
10165
|
-
*/
|
|
10166
|
-
/**
|
|
10167
|
-
* Select the candidate with the highest effective staleness.
|
|
10168
|
-
*
|
|
10169
|
-
* @param candidates - Array of candidates with computed effective staleness.
|
|
10170
|
-
* @returns The winning candidate, or null if no candidates.
|
|
10171
|
-
*/
|
|
10172
|
-
function selectCandidate(candidates) {
|
|
10173
|
-
if (candidates.length === 0)
|
|
10174
|
-
return null;
|
|
10175
|
-
let best = candidates[0];
|
|
10176
|
-
for (let i = 1; i < candidates.length; i++) {
|
|
10177
|
-
if (candidates[i].effectiveStaleness > best.effectiveStaleness) {
|
|
10178
|
-
best = candidates[i];
|
|
10179
|
-
}
|
|
10180
|
-
}
|
|
10181
|
-
return best;
|
|
10182
|
-
}
|
|
10183
|
-
/**
|
|
10184
|
-
* Extract stale candidates from a list and return the stalest path.
|
|
10185
|
-
*
|
|
10186
|
-
* Consolidates the repeated pattern of:
|
|
10187
|
-
* filter → computeEffectiveStaleness → selectCandidate → return path
|
|
10188
|
-
*
|
|
10189
|
-
* @param candidates - Array with node, meta, and stalenessSeconds.
|
|
10190
|
-
* @param depthWeight - Depth weighting exponent from config.
|
|
10191
|
-
* @returns The stalest candidate's metaPath, or null if none are stale.
|
|
10192
|
-
*/
|
|
10193
|
-
function discoverStalestPath(candidates, depthWeight) {
|
|
10194
|
-
const weighted = computeEffectiveStaleness(candidates, depthWeight);
|
|
10195
|
-
const winner = selectCandidate(weighted);
|
|
10196
|
-
return winner?.node.metaPath ?? null;
|
|
10197
|
-
}
|
|
10198
|
-
|
|
10199
|
-
/**
|
|
10200
|
-
* Shared error utilities.
|
|
10201
|
-
*
|
|
10202
|
-
* @module errors
|
|
10203
|
-
*/
|
|
10204
|
-
/**
|
|
10205
|
-
* Wrap an unknown caught value into a MetaError.
|
|
10206
|
-
*
|
|
10207
|
-
* @param step - Which synthesis step failed.
|
|
10208
|
-
* @param err - The caught error value.
|
|
10209
|
-
* @param code - Error classification code.
|
|
10210
|
-
* @returns A structured MetaError.
|
|
10211
|
-
*/
|
|
10212
|
-
function toMetaError(step, err, code = 'FAILED') {
|
|
10213
|
-
return {
|
|
10214
|
-
step,
|
|
10215
|
-
code,
|
|
10216
|
-
message: err instanceof Error ? err.message : String(err),
|
|
10217
|
-
};
|
|
10218
|
-
}
|
|
10219
|
-
|
|
10220
|
-
/**
|
|
10221
|
-
* Compute a structure hash from a sorted file listing.
|
|
10222
|
-
*
|
|
10223
|
-
* Used to detect when directory structure changes, triggering
|
|
10224
|
-
* an architect re-run.
|
|
10225
|
-
*
|
|
10226
|
-
* @module structureHash
|
|
10227
|
-
*/
|
|
10228
|
-
/**
|
|
10229
|
-
* Compute a SHA-256 hash of a sorted file listing.
|
|
10230
|
-
*
|
|
10231
|
-
* @param filePaths - Array of file paths in scope.
|
|
10232
|
-
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
10233
|
-
*/
|
|
10234
|
-
function computeStructureHash(filePaths) {
|
|
10235
|
-
const sorted = [...filePaths].sort();
|
|
10236
|
-
const content = sorted.join('\n');
|
|
10237
|
-
return createHash('sha256').update(content).digest('hex');
|
|
10238
|
-
}
|
|
10239
|
-
|
|
10240
|
-
/**
|
|
10241
|
-
* Parse subprocess outputs for each synthesis step.
|
|
10242
|
-
*
|
|
10243
|
-
* - Architect: returns text \> _builder
|
|
10244
|
-
* - Builder: returns JSON \> _content + structured fields
|
|
10245
|
-
* - Critic: returns text \> _feedback
|
|
10246
|
-
*
|
|
10247
|
-
* @module orchestrator/parseOutput
|
|
10248
|
-
*/
|
|
10249
|
-
/**
|
|
10250
|
-
* Parse architect output. The architect returns a task brief as text.
|
|
10251
|
-
*
|
|
10252
|
-
* @param output - Raw subprocess output.
|
|
10253
|
-
* @returns The task brief string.
|
|
10254
|
-
*/
|
|
10255
|
-
function parseArchitectOutput(output) {
|
|
10256
|
-
return output.trim();
|
|
10257
|
-
}
|
|
10258
|
-
/**
|
|
10259
|
-
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
10260
|
-
*
|
|
10261
|
-
* Attempts JSON parse first. If that fails, treats the entire output as _content.
|
|
10262
|
-
*
|
|
10263
|
-
* @param output - Raw subprocess output.
|
|
10264
|
-
* @returns Parsed builder output with content and structured fields.
|
|
10265
|
-
*/
|
|
10266
|
-
function parseBuilderOutput(output) {
|
|
10267
|
-
const trimmed = output.trim();
|
|
10268
|
-
// Strategy 1: Try to parse the entire output as JSON directly
|
|
10269
|
-
const direct = tryParseJson(trimmed);
|
|
10270
|
-
if (direct)
|
|
10271
|
-
return direct;
|
|
10272
|
-
// Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
|
|
10273
|
-
const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
10274
|
-
const fenceMatches = [];
|
|
10275
|
-
let match;
|
|
10276
|
-
while ((match = fencePattern.exec(trimmed)) !== null) {
|
|
10277
|
-
fenceMatches.push(match[1].trim());
|
|
10278
|
-
}
|
|
10279
|
-
// Try last fence first (most likely to be the actual output)
|
|
10280
|
-
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
10281
|
-
const result = tryParseJson(fenceMatches[i]);
|
|
10282
|
-
if (result)
|
|
10283
|
-
return result;
|
|
10284
|
-
}
|
|
10285
|
-
// Strategy 3: Find outermost { ... } braces
|
|
10286
|
-
const firstBrace = trimmed.indexOf('{');
|
|
10287
|
-
const lastBrace = trimmed.lastIndexOf('}');
|
|
10288
|
-
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
10289
|
-
const result = tryParseJson(trimmed.substring(firstBrace, lastBrace + 1));
|
|
10290
|
-
if (result)
|
|
10291
|
-
return result;
|
|
10292
|
-
}
|
|
10293
|
-
// Fallback: treat entire output as content
|
|
10294
|
-
return { content: trimmed, fields: {} };
|
|
10295
|
-
}
|
|
10296
|
-
/** Try to parse a string as JSON and extract builder output fields. */
|
|
10297
|
-
function tryParseJson(str) {
|
|
10298
|
-
try {
|
|
10299
|
-
const raw = JSON.parse(str);
|
|
10300
|
-
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
10301
|
-
return null;
|
|
10302
|
-
}
|
|
10303
|
-
const parsed = raw;
|
|
10304
|
-
// Extract _content
|
|
10305
|
-
const content = typeof parsed['_content'] === 'string'
|
|
10306
|
-
? parsed['_content']
|
|
10307
|
-
: typeof parsed['content'] === 'string'
|
|
10308
|
-
? parsed['content']
|
|
10309
|
-
: null;
|
|
10310
|
-
if (content === null)
|
|
10311
|
-
return null;
|
|
10312
|
-
// Extract _state (the ONLY underscore key the builder is allowed to set)
|
|
10313
|
-
const state = '_state' in parsed ? parsed['_state'] : undefined;
|
|
10314
|
-
// Extract non-underscore fields
|
|
10315
|
-
const fields = {};
|
|
10316
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
10317
|
-
if (!key.startsWith('_') && key !== 'content') {
|
|
10318
|
-
fields[key] = value;
|
|
10319
|
-
}
|
|
10320
|
-
}
|
|
10321
|
-
return { content, fields, ...(state !== undefined ? { state } : {}) };
|
|
10322
|
-
}
|
|
10323
|
-
catch {
|
|
10324
|
-
return null;
|
|
10325
|
-
}
|
|
10326
|
-
}
|
|
10327
|
-
/**
|
|
10328
|
-
* Parse critic output. The critic returns evaluation text.
|
|
10329
|
-
*
|
|
10330
|
-
* @param output - Raw subprocess output.
|
|
10331
|
-
* @returns The feedback string.
|
|
10332
|
-
*/
|
|
10333
|
-
function parseCriticOutput(output) {
|
|
10334
|
-
return output.trim();
|
|
10335
|
-
}
|
|
10336
|
-
|
|
10337
10145
|
/**
|
|
10338
10146
|
* Pure phase-state transition functions.
|
|
10339
10147
|
*
|
|
@@ -10383,7 +10191,42 @@ function enforceInvariant(state) {
|
|
|
10383
10191
|
// running in non-first position would be a bug, but don't mask it
|
|
10384
10192
|
}
|
|
10385
10193
|
}
|
|
10386
|
-
return result;
|
|
10194
|
+
return result;
|
|
10195
|
+
}
|
|
10196
|
+
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
10197
|
+
/**
|
|
10198
|
+
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
10199
|
+
* Triggers: _structureHash change, _steer change, _architect change,
|
|
10200
|
+
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
10201
|
+
*/
|
|
10202
|
+
function invalidateArchitect(state) {
|
|
10203
|
+
return enforceInvariant({
|
|
10204
|
+
architect: state.architect === 'failed' ? 'failed' : 'pending',
|
|
10205
|
+
builder: state.builder === 'fresh' ? 'stale' : state.builder,
|
|
10206
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
10207
|
+
});
|
|
10208
|
+
}
|
|
10209
|
+
/**
|
|
10210
|
+
* Builder invalidated (scope mtime or cross-ref _content change):
|
|
10211
|
+
* builder → pending; critic → stale.
|
|
10212
|
+
* Only applies when architect is fresh; otherwise, builder stays stale.
|
|
10213
|
+
*/
|
|
10214
|
+
function invalidateBuilder(state) {
|
|
10215
|
+
if (state.architect !== 'fresh') {
|
|
10216
|
+
// Architect is not fresh — builder stays stale (or whatever it is)
|
|
10217
|
+
return enforceInvariant({
|
|
10218
|
+
...state,
|
|
10219
|
+
builder: state.builder === 'fresh' || state.builder === 'stale'
|
|
10220
|
+
? 'stale'
|
|
10221
|
+
: state.builder,
|
|
10222
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
10223
|
+
});
|
|
10224
|
+
}
|
|
10225
|
+
return enforceInvariant({
|
|
10226
|
+
...state,
|
|
10227
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
10228
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
10229
|
+
});
|
|
10387
10230
|
}
|
|
10388
10231
|
// ── Phase success transitions ──────────────────────────────────────────
|
|
10389
10232
|
/**
|
|
@@ -10553,7 +10396,9 @@ function derivePhaseState(meta, inputs) {
|
|
|
10553
10396
|
}
|
|
10554
10397
|
// Check architect invalidation (when inputs are provided)
|
|
10555
10398
|
if (inputs) {
|
|
10556
|
-
|
|
10399
|
+
// Progressive metas: structure changes invalidate builder, not architect
|
|
10400
|
+
const structureInvalidatesArchitect = inputs.structureChanged && meta._state === undefined;
|
|
10401
|
+
const architectInvalidated = structureInvalidatesArchitect ||
|
|
10557
10402
|
inputs.steerChanged ||
|
|
10558
10403
|
inputs.architectChanged ||
|
|
10559
10404
|
inputs.crossRefsChanged ||
|
|
@@ -10565,6 +10410,14 @@ function derivePhaseState(meta, inputs) {
|
|
|
10565
10410
|
critic: 'stale',
|
|
10566
10411
|
};
|
|
10567
10412
|
}
|
|
10413
|
+
// Progressive meta with structure change: builder-only invalidation
|
|
10414
|
+
if (inputs.structureChanged && meta._state !== undefined) {
|
|
10415
|
+
return {
|
|
10416
|
+
architect: 'fresh',
|
|
10417
|
+
builder: 'pending',
|
|
10418
|
+
critic: 'stale',
|
|
10419
|
+
};
|
|
10420
|
+
}
|
|
10568
10421
|
}
|
|
10569
10422
|
// Has _builder but no _content: builder is pending
|
|
10570
10423
|
if (meta._builder && !meta._content) {
|
|
@@ -10586,6 +10439,154 @@ function derivePhaseState(meta, inputs) {
|
|
|
10586
10439
|
return freshPhaseState();
|
|
10587
10440
|
}
|
|
10588
10441
|
|
|
10442
|
+
/**
|
|
10443
|
+
* Compute a structure hash from a sorted file listing.
|
|
10444
|
+
*
|
|
10445
|
+
* Used to detect when directory structure changes, triggering
|
|
10446
|
+
* an architect re-run.
|
|
10447
|
+
*
|
|
10448
|
+
* @module structureHash
|
|
10449
|
+
*/
|
|
10450
|
+
/**
|
|
10451
|
+
* Compute a SHA-256 hash of a sorted file listing.
|
|
10452
|
+
*
|
|
10453
|
+
* @param filePaths - Array of file paths in scope.
|
|
10454
|
+
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
10455
|
+
*/
|
|
10456
|
+
function computeStructureHash(filePaths) {
|
|
10457
|
+
const sorted = [...filePaths].sort();
|
|
10458
|
+
const content = sorted.join('\n');
|
|
10459
|
+
return createHash('sha256').update(content).digest('hex');
|
|
10460
|
+
}
|
|
10461
|
+
|
|
10462
|
+
/**
|
|
10463
|
+
* Per-tick invalidation pass.
|
|
10464
|
+
*
|
|
10465
|
+
* Computes architect-invalidating and builder-invalidating inputs for a meta,
|
|
10466
|
+
* then applies the cascade to update _phaseState.
|
|
10467
|
+
*
|
|
10468
|
+
* @module phaseState/invalidate
|
|
10469
|
+
*/
|
|
10470
|
+
/**
|
|
10471
|
+
* Compute invalidation inputs and apply cascade for a single meta.
|
|
10472
|
+
*
|
|
10473
|
+
* @param meta - Current meta.json content with existing _phaseState.
|
|
10474
|
+
* @param scopeFiles - Sorted file list from scope.
|
|
10475
|
+
* @param config - MetaConfig for architectEvery.
|
|
10476
|
+
* @param node - MetaNode for archive access.
|
|
10477
|
+
* @param crossRefMetas - Map of cross-ref owner paths to their current _content.
|
|
10478
|
+
* @param archiveCrossRefContent - Map of cross-ref owner paths to their archived _content.
|
|
10479
|
+
* @returns Updated phase state and invalidation details.
|
|
10480
|
+
*/
|
|
10481
|
+
async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas, archiveCrossRefContent) {
|
|
10482
|
+
let phaseState = meta._phaseState ?? {
|
|
10483
|
+
architect: 'fresh',
|
|
10484
|
+
builder: 'fresh',
|
|
10485
|
+
critic: 'fresh',
|
|
10486
|
+
};
|
|
10487
|
+
// ── Architect-level inputs ──
|
|
10488
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
10489
|
+
const structureChanged = structureHash !== meta._structureHash;
|
|
10490
|
+
const latestArchive = await readLatestArchive(node.metaPath);
|
|
10491
|
+
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10492
|
+
// _architect change: compare current vs. archive
|
|
10493
|
+
const architectChanged = latestArchive
|
|
10494
|
+
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
10495
|
+
: Boolean(meta._architect);
|
|
10496
|
+
// _crossRefs declaration change
|
|
10497
|
+
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
10498
|
+
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
10499
|
+
.slice()
|
|
10500
|
+
.sort()
|
|
10501
|
+
.join(',');
|
|
10502
|
+
const crossRefsDeclChanged = latestArchive
|
|
10503
|
+
? currentRefs !== archiveRefs
|
|
10504
|
+
: currentRefs.length > 0;
|
|
10505
|
+
const architectInvalidators = [];
|
|
10506
|
+
if (structureChanged) {
|
|
10507
|
+
if (meta._state !== undefined) {
|
|
10508
|
+
// Progressive entity: new files → builder only (cursor handles incremental)
|
|
10509
|
+
phaseState = invalidateBuilder(phaseState);
|
|
10510
|
+
}
|
|
10511
|
+
else {
|
|
10512
|
+
architectInvalidators.push('structureHash');
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
if (steerChanged)
|
|
10516
|
+
architectInvalidators.push('steer');
|
|
10517
|
+
if (architectChanged)
|
|
10518
|
+
architectInvalidators.push('_architect');
|
|
10519
|
+
if (crossRefsDeclChanged)
|
|
10520
|
+
architectInvalidators.push('_crossRefs');
|
|
10521
|
+
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
10522
|
+
architectInvalidators.push('architectEvery');
|
|
10523
|
+
}
|
|
10524
|
+
// First-run check: no _builder means architect must run
|
|
10525
|
+
const firstRun = !meta._builder;
|
|
10526
|
+
if (architectInvalidators.length > 0 || firstRun) {
|
|
10527
|
+
phaseState = invalidateArchitect(phaseState);
|
|
10528
|
+
}
|
|
10529
|
+
// ── Builder-level inputs ──
|
|
10530
|
+
// Scope file mtime check — if any file newer than _generatedAt
|
|
10531
|
+
const scopeMtimeMax = null;
|
|
10532
|
+
// Note: actual mtime check is done by the caller or via isStale;
|
|
10533
|
+
// here we just detect cross-ref content changes for the cascade.
|
|
10534
|
+
// Cross-ref _content change (builder-invalidating)
|
|
10535
|
+
let crossRefContentChanged = false;
|
|
10536
|
+
return {
|
|
10537
|
+
phaseState,
|
|
10538
|
+
architectInvalidators,
|
|
10539
|
+
stalenessInputs: {
|
|
10540
|
+
structureHash,
|
|
10541
|
+
steerChanged,
|
|
10542
|
+
architectChanged,
|
|
10543
|
+
crossRefsDeclChanged,
|
|
10544
|
+
scopeMtimeMax,
|
|
10545
|
+
crossRefContentChanged,
|
|
10546
|
+
},
|
|
10547
|
+
structureHash,
|
|
10548
|
+
steerChanged,
|
|
10549
|
+
};
|
|
10550
|
+
}
|
|
10551
|
+
|
|
10552
|
+
/**
|
|
10553
|
+
* Weighted staleness formula for candidate selection.
|
|
10554
|
+
*
|
|
10555
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10556
|
+
*
|
|
10557
|
+
* @module scheduling/weightedFormula
|
|
10558
|
+
*/
|
|
10559
|
+
/**
|
|
10560
|
+
* Compute effective staleness for a set of candidates.
|
|
10561
|
+
*
|
|
10562
|
+
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
10563
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10564
|
+
*
|
|
10565
|
+
* Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
|
|
10566
|
+
* metas to tune how much their tree position affects scheduling.
|
|
10567
|
+
*
|
|
10568
|
+
* @param candidates - Array of \{ node, meta, actualStaleness \}.
|
|
10569
|
+
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
10570
|
+
* @returns Same array with effectiveStaleness computed.
|
|
10571
|
+
*/
|
|
10572
|
+
function computeEffectiveStaleness(candidates, depthWeight) {
|
|
10573
|
+
if (candidates.length === 0)
|
|
10574
|
+
return [];
|
|
10575
|
+
// Get depth for each candidate: use _depth override or tree depth
|
|
10576
|
+
const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
|
|
10577
|
+
// Normalize: shift so minimum becomes 0
|
|
10578
|
+
const minDepth = Math.min(...depths);
|
|
10579
|
+
const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
|
|
10580
|
+
return candidates.map((c, i) => {
|
|
10581
|
+
const emphasis = c.meta._emphasis ?? 1;
|
|
10582
|
+
return {
|
|
10583
|
+
...c,
|
|
10584
|
+
effectiveStaleness: c.actualStaleness *
|
|
10585
|
+
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
10586
|
+
};
|
|
10587
|
+
});
|
|
10588
|
+
}
|
|
10589
|
+
|
|
10589
10590
|
/**
|
|
10590
10591
|
* Corpus-wide phase scheduler.
|
|
10591
10592
|
*
|
|
@@ -10598,18 +10599,30 @@ function derivePhaseState(meta, inputs) {
|
|
|
10598
10599
|
/**
|
|
10599
10600
|
* Build phase candidates from listMetas entries.
|
|
10600
10601
|
*
|
|
10601
|
-
* Derives phase state
|
|
10602
|
+
* Derives phase state, auto-retries failed phases, and applies Tier 1
|
|
10603
|
+
* cheap-invalidation (no I/O) for metas with persisted _phaseState.
|
|
10602
10604
|
* Used by orchestratePhase, queue route, and status route.
|
|
10603
10605
|
*/
|
|
10604
|
-
function buildPhaseCandidates(entries) {
|
|
10605
|
-
return entries.map((entry) =>
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10606
|
+
function buildPhaseCandidates(entries, architectEvery) {
|
|
10607
|
+
return entries.map((entry) => {
|
|
10608
|
+
let ps = retryAllFailed(derivePhaseState(entry.meta));
|
|
10609
|
+
// Tier 1 cheap invalidation for metas with persisted _phaseState
|
|
10610
|
+
if (entry.meta._phaseState) {
|
|
10611
|
+
const needsArchitect = !entry.meta._builder ||
|
|
10612
|
+
(entry.meta._synthesisCount ?? 0) >= architectEvery;
|
|
10613
|
+
if (needsArchitect && ps.architect === 'fresh') {
|
|
10614
|
+
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
return {
|
|
10618
|
+
node: entry.node,
|
|
10619
|
+
meta: entry.meta,
|
|
10620
|
+
phaseState: ps,
|
|
10621
|
+
actualStaleness: entry.stalenessSeconds,
|
|
10622
|
+
locked: entry.locked,
|
|
10623
|
+
disabled: entry.disabled,
|
|
10624
|
+
};
|
|
10625
|
+
});
|
|
10613
10626
|
}
|
|
10614
10627
|
/**
|
|
10615
10628
|
* Rank all eligible phase candidates by priority.
|
|
@@ -10672,6 +10685,124 @@ function selectPhaseCandidate(metas, depthWeight) {
|
|
|
10672
10685
|
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
10673
10686
|
}
|
|
10674
10687
|
|
|
10688
|
+
/**
|
|
10689
|
+
* Shared error utilities.
|
|
10690
|
+
*
|
|
10691
|
+
* @module errors
|
|
10692
|
+
*/
|
|
10693
|
+
/**
|
|
10694
|
+
* Wrap an unknown caught value into a MetaError.
|
|
10695
|
+
*
|
|
10696
|
+
* @param step - Which synthesis step failed.
|
|
10697
|
+
* @param err - The caught error value.
|
|
10698
|
+
* @param code - Error classification code.
|
|
10699
|
+
* @returns A structured MetaError.
|
|
10700
|
+
*/
|
|
10701
|
+
function toMetaError(step, err, code = 'FAILED') {
|
|
10702
|
+
return {
|
|
10703
|
+
step,
|
|
10704
|
+
code,
|
|
10705
|
+
message: err instanceof Error ? err.message : String(err),
|
|
10706
|
+
};
|
|
10707
|
+
}
|
|
10708
|
+
|
|
10709
|
+
/**
|
|
10710
|
+
* Parse subprocess outputs for each synthesis step.
|
|
10711
|
+
*
|
|
10712
|
+
* - Architect: returns text \> _builder
|
|
10713
|
+
* - Builder: returns JSON \> _content + structured fields
|
|
10714
|
+
* - Critic: returns text \> _feedback
|
|
10715
|
+
*
|
|
10716
|
+
* @module orchestrator/parseOutput
|
|
10717
|
+
*/
|
|
10718
|
+
/**
|
|
10719
|
+
* Parse architect output. The architect returns a task brief as text.
|
|
10720
|
+
*
|
|
10721
|
+
* @param output - Raw subprocess output.
|
|
10722
|
+
* @returns The task brief string.
|
|
10723
|
+
*/
|
|
10724
|
+
function parseArchitectOutput(output) {
|
|
10725
|
+
return output.trim();
|
|
10726
|
+
}
|
|
10727
|
+
/**
|
|
10728
|
+
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
10729
|
+
*
|
|
10730
|
+
* Attempts JSON parse first. If that fails, treats the entire output as _content.
|
|
10731
|
+
*
|
|
10732
|
+
* @param output - Raw subprocess output.
|
|
10733
|
+
* @returns Parsed builder output with content and structured fields.
|
|
10734
|
+
*/
|
|
10735
|
+
function parseBuilderOutput(output) {
|
|
10736
|
+
const trimmed = output.trim();
|
|
10737
|
+
// Strategy 1: Try to parse the entire output as JSON directly
|
|
10738
|
+
const direct = tryParseJson(trimmed);
|
|
10739
|
+
if (direct)
|
|
10740
|
+
return direct;
|
|
10741
|
+
// Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
|
|
10742
|
+
const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
10743
|
+
const fenceMatches = [];
|
|
10744
|
+
let match;
|
|
10745
|
+
while ((match = fencePattern.exec(trimmed)) !== null) {
|
|
10746
|
+
fenceMatches.push(match[1].trim());
|
|
10747
|
+
}
|
|
10748
|
+
// Try last fence first (most likely to be the actual output)
|
|
10749
|
+
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
10750
|
+
const result = tryParseJson(fenceMatches[i]);
|
|
10751
|
+
if (result)
|
|
10752
|
+
return result;
|
|
10753
|
+
}
|
|
10754
|
+
// Strategy 3: Find outermost { ... } braces
|
|
10755
|
+
const firstBrace = trimmed.indexOf('{');
|
|
10756
|
+
const lastBrace = trimmed.lastIndexOf('}');
|
|
10757
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
10758
|
+
const result = tryParseJson(trimmed.substring(firstBrace, lastBrace + 1));
|
|
10759
|
+
if (result)
|
|
10760
|
+
return result;
|
|
10761
|
+
}
|
|
10762
|
+
// Fallback: treat entire output as content
|
|
10763
|
+
return { content: trimmed, fields: {} };
|
|
10764
|
+
}
|
|
10765
|
+
/** Try to parse a string as JSON and extract builder output fields. */
|
|
10766
|
+
function tryParseJson(str) {
|
|
10767
|
+
try {
|
|
10768
|
+
const raw = JSON.parse(str);
|
|
10769
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
10770
|
+
return null;
|
|
10771
|
+
}
|
|
10772
|
+
const parsed = raw;
|
|
10773
|
+
// Extract _content
|
|
10774
|
+
const content = typeof parsed['_content'] === 'string'
|
|
10775
|
+
? parsed['_content']
|
|
10776
|
+
: typeof parsed['content'] === 'string'
|
|
10777
|
+
? parsed['content']
|
|
10778
|
+
: null;
|
|
10779
|
+
if (content === null)
|
|
10780
|
+
return null;
|
|
10781
|
+
// Extract _state (the ONLY underscore key the builder is allowed to set)
|
|
10782
|
+
const state = '_state' in parsed ? parsed['_state'] : undefined;
|
|
10783
|
+
// Extract non-underscore fields
|
|
10784
|
+
const fields = {};
|
|
10785
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
10786
|
+
if (!key.startsWith('_') && key !== 'content') {
|
|
10787
|
+
fields[key] = value;
|
|
10788
|
+
}
|
|
10789
|
+
}
|
|
10790
|
+
return { content, fields, ...(state !== undefined ? { state } : {}) };
|
|
10791
|
+
}
|
|
10792
|
+
catch {
|
|
10793
|
+
return null;
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
10796
|
+
/**
|
|
10797
|
+
* Parse critic output. The critic returns evaluation text.
|
|
10798
|
+
*
|
|
10799
|
+
* @param output - Raw subprocess output.
|
|
10800
|
+
* @returns The feedback string.
|
|
10801
|
+
*/
|
|
10802
|
+
function parseCriticOutput(output) {
|
|
10803
|
+
return output.trim();
|
|
10804
|
+
}
|
|
10805
|
+
|
|
10675
10806
|
/**
|
|
10676
10807
|
* Per-phase executors for the phase-state machine.
|
|
10677
10808
|
*
|
|
@@ -10930,7 +11061,7 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
|
|
|
10930
11061
|
if (metaResult.entries.length === 0)
|
|
10931
11062
|
return { executed: false };
|
|
10932
11063
|
// Build candidates with phase state (including invalidation + auto-retry)
|
|
10933
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
11064
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
10934
11065
|
// Select best phase candidate
|
|
10935
11066
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
10936
11067
|
if (!winner) {
|
|
@@ -11605,46 +11736,6 @@ function buildMetaRules(config) {
|
|
|
11605
11736
|
},
|
|
11606
11737
|
renderAs: 'md',
|
|
11607
11738
|
},
|
|
11608
|
-
{
|
|
11609
|
-
name: 'meta-config',
|
|
11610
|
-
description: 'jeeves-meta configuration file',
|
|
11611
|
-
match: {
|
|
11612
|
-
properties: {
|
|
11613
|
-
file: {
|
|
11614
|
-
properties: {
|
|
11615
|
-
path: {
|
|
11616
|
-
type: 'string',
|
|
11617
|
-
glob: '**/jeeves-meta{.config.json,/config.json}',
|
|
11618
|
-
},
|
|
11619
|
-
},
|
|
11620
|
-
},
|
|
11621
|
-
},
|
|
11622
|
-
},
|
|
11623
|
-
schema: ['base', { properties: { domains: { set: ['meta-config'] } } }],
|
|
11624
|
-
render: {
|
|
11625
|
-
frontmatter: [
|
|
11626
|
-
'watcherUrl',
|
|
11627
|
-
'gatewayUrl',
|
|
11628
|
-
'architectEvery',
|
|
11629
|
-
'depthWeight',
|
|
11630
|
-
'maxArchive',
|
|
11631
|
-
'maxLines',
|
|
11632
|
-
],
|
|
11633
|
-
body: [
|
|
11634
|
-
{
|
|
11635
|
-
path: 'json.defaultArchitect',
|
|
11636
|
-
heading: 2,
|
|
11637
|
-
label: 'Default Architect Prompt',
|
|
11638
|
-
},
|
|
11639
|
-
{
|
|
11640
|
-
path: 'json.defaultCritic',
|
|
11641
|
-
heading: 2,
|
|
11642
|
-
label: 'Default Critic Prompt',
|
|
11643
|
-
},
|
|
11644
|
-
],
|
|
11645
|
-
},
|
|
11646
|
-
renderAs: 'md',
|
|
11647
|
-
},
|
|
11648
11739
|
];
|
|
11649
11740
|
}
|
|
11650
11741
|
/**
|
|
@@ -11888,13 +11979,15 @@ class Scheduler {
|
|
|
11888
11979
|
queue;
|
|
11889
11980
|
logger;
|
|
11890
11981
|
watcher;
|
|
11982
|
+
cache;
|
|
11891
11983
|
registrar = null;
|
|
11892
11984
|
currentExpression;
|
|
11893
|
-
constructor(config, queue, logger, watcher) {
|
|
11985
|
+
constructor(config, queue, logger, watcher, cache) {
|
|
11894
11986
|
this.config = config;
|
|
11895
11987
|
this.queue = queue;
|
|
11896
11988
|
this.logger = logger;
|
|
11897
11989
|
this.watcher = watcher;
|
|
11990
|
+
this.cache = cache;
|
|
11898
11991
|
this.currentExpression = config.schedule;
|
|
11899
11992
|
}
|
|
11900
11993
|
/** Set the rule registrar for watcher restart detection. */
|
|
@@ -12011,8 +12104,8 @@ class Scheduler {
|
|
|
12011
12104
|
*/
|
|
12012
12105
|
async discoverNextPhase() {
|
|
12013
12106
|
try {
|
|
12014
|
-
const result = await
|
|
12015
|
-
const candidates = buildPhaseCandidates(result.entries);
|
|
12107
|
+
const result = await this.cache.get(this.config, this.watcher);
|
|
12108
|
+
const candidates = buildPhaseCandidates(result.entries, this.config.architectEvery);
|
|
12016
12109
|
const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
|
|
12017
12110
|
if (!winner)
|
|
12018
12111
|
return null;
|
|
@@ -12437,11 +12530,11 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12437
12530
|
*/
|
|
12438
12531
|
function registerPreviewRoute(app, deps) {
|
|
12439
12532
|
app.get('/preview', async (request, reply) => {
|
|
12440
|
-
const { config, watcher } = deps;
|
|
12533
|
+
const { config, watcher, cache } = deps;
|
|
12441
12534
|
const query = request.query;
|
|
12442
12535
|
let result;
|
|
12443
12536
|
try {
|
|
12444
|
-
result = await
|
|
12537
|
+
result = await cache.get(config, watcher);
|
|
12445
12538
|
}
|
|
12446
12539
|
catch {
|
|
12447
12540
|
return reply.status(503).send({
|
|
@@ -12461,40 +12554,24 @@ function registerPreviewRoute(app, deps) {
|
|
|
12461
12554
|
}
|
|
12462
12555
|
}
|
|
12463
12556
|
else {
|
|
12464
|
-
// Select
|
|
12465
|
-
const
|
|
12466
|
-
|
|
12467
|
-
|
|
12468
|
-
node: e.node,
|
|
12469
|
-
meta: e.meta,
|
|
12470
|
-
actualStaleness: e.stalenessSeconds,
|
|
12471
|
-
}));
|
|
12472
|
-
const stalestPath = discoverStalestPath(stale, config.depthWeight);
|
|
12473
|
-
if (!stalestPath) {
|
|
12557
|
+
// Select best phase candidate
|
|
12558
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
12559
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12560
|
+
if (!winner) {
|
|
12474
12561
|
return { message: 'No stale metas found. Nothing to synthesize.' };
|
|
12475
12562
|
}
|
|
12476
|
-
targetNode = findNode(result.tree,
|
|
12563
|
+
targetNode = findNode(result.tree, winner.node.metaPath);
|
|
12477
12564
|
}
|
|
12478
12565
|
const meta = await readMetaJson(targetNode.metaPath);
|
|
12479
12566
|
// Scope files
|
|
12480
12567
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
12481
|
-
|
|
12568
|
+
// Compute invalidation inputs (DRY: reuse phaseState/invalidate logic)
|
|
12569
|
+
const invalidation = await computeInvalidation(meta, scopeFiles, config, targetNode);
|
|
12570
|
+
const { architectInvalidators, stalenessInputs } = invalidation;
|
|
12571
|
+
const { structureHash } = invalidation;
|
|
12482
12572
|
const structureChanged = structureHash !== meta._structureHash;
|
|
12483
|
-
const
|
|
12484
|
-
const
|
|
12485
|
-
// _architect change detection
|
|
12486
|
-
const architectChanged = latestArchive
|
|
12487
|
-
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
12488
|
-
: Boolean(meta._architect);
|
|
12489
|
-
// _crossRefs declaration change detection
|
|
12490
|
-
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
12491
|
-
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
12492
|
-
.slice()
|
|
12493
|
-
.sort()
|
|
12494
|
-
.join(',');
|
|
12495
|
-
const crossRefsDeclChanged = latestArchive
|
|
12496
|
-
? currentRefs !== archiveRefs
|
|
12497
|
-
: currentRefs.length > 0;
|
|
12573
|
+
const { steerChanged } = invalidation;
|
|
12574
|
+
const { architectChanged, crossRefsDeclChanged } = stalenessInputs;
|
|
12498
12575
|
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
12499
12576
|
// Delta files
|
|
12500
12577
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
@@ -12519,30 +12596,6 @@ function registerPreviewRoute(app, deps) {
|
|
|
12519
12596
|
});
|
|
12520
12597
|
const owedPhase = getOwedPhase(phaseState);
|
|
12521
12598
|
const priorityBand = getPriorityBand(phaseState);
|
|
12522
|
-
// Architect invalidators
|
|
12523
|
-
const architectInvalidators = [];
|
|
12524
|
-
if (owedPhase === 'architect') {
|
|
12525
|
-
if (structureChanged)
|
|
12526
|
-
architectInvalidators.push('structureHash');
|
|
12527
|
-
if (steerChanged)
|
|
12528
|
-
architectInvalidators.push('steer');
|
|
12529
|
-
if (architectChanged)
|
|
12530
|
-
architectInvalidators.push('_architect');
|
|
12531
|
-
if (crossRefsDeclChanged)
|
|
12532
|
-
architectInvalidators.push('_crossRefs');
|
|
12533
|
-
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
12534
|
-
architectInvalidators.push('architectEvery');
|
|
12535
|
-
}
|
|
12536
|
-
}
|
|
12537
|
-
// Staleness inputs
|
|
12538
|
-
const stalenessInputs = {
|
|
12539
|
-
structureHash,
|
|
12540
|
-
steerChanged,
|
|
12541
|
-
architectChanged,
|
|
12542
|
-
crossRefsDeclChanged,
|
|
12543
|
-
scopeMtimeMax: null,
|
|
12544
|
-
crossRefContentChanged: false,
|
|
12545
|
-
};
|
|
12546
12599
|
return {
|
|
12547
12600
|
path: targetNode.metaPath,
|
|
12548
12601
|
staleness: {
|
|
@@ -12616,8 +12669,8 @@ function registerQueueRoutes(app, deps) {
|
|
|
12616
12669
|
// ranked by scheduler priority (computed on read, not persisted)
|
|
12617
12670
|
let automatic = [];
|
|
12618
12671
|
try {
|
|
12619
|
-
const metaResult = await
|
|
12620
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12672
|
+
const metaResult = await deps.cache.get(deps.config, deps.watcher);
|
|
12673
|
+
const candidates = buildPhaseCandidates(metaResult.entries, deps.config.architectEvery);
|
|
12621
12674
|
const ranked = rankPhaseCandidates(candidates, deps.config.depthWeight);
|
|
12622
12675
|
automatic = ranked.map((c) => ({
|
|
12623
12676
|
path: c.node.metaPath,
|
|
@@ -12840,7 +12893,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12840
12893
|
name: SERVICE_NAME,
|
|
12841
12894
|
version: SERVICE_VERSION,
|
|
12842
12895
|
getHealth: async () => {
|
|
12843
|
-
const { config, queue, scheduler, stats, watcher } = deps;
|
|
12896
|
+
const { config, queue, scheduler, stats, watcher, cache } = deps;
|
|
12844
12897
|
// On-demand dependency checks
|
|
12845
12898
|
const [watcherHealth, gatewayHealth] = await Promise.all([
|
|
12846
12899
|
checkWatcher(config.watcherUrl),
|
|
@@ -12854,7 +12907,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12854
12907
|
};
|
|
12855
12908
|
let nextPhase = null;
|
|
12856
12909
|
try {
|
|
12857
|
-
const metaResult = await
|
|
12910
|
+
const metaResult = await cache.get(config, watcher);
|
|
12858
12911
|
// Count raw phase states (before retry) for display
|
|
12859
12912
|
for (const entry of metaResult.entries) {
|
|
12860
12913
|
const ps = derivePhaseState(entry.meta);
|
|
@@ -12863,7 +12916,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12863
12916
|
}
|
|
12864
12917
|
}
|
|
12865
12918
|
// Build candidates (with auto-retry) for scheduling
|
|
12866
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12919
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
12867
12920
|
// Find next phase candidate
|
|
12868
12921
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12869
12922
|
if (winner) {
|
|
@@ -12926,7 +12979,7 @@ const synthesizeBodySchema = z.object({
|
|
|
12926
12979
|
function registerSynthesizeRoute(app, deps) {
|
|
12927
12980
|
app.post('/synthesize', async (request, reply) => {
|
|
12928
12981
|
const body = synthesizeBodySchema.parse(request.body);
|
|
12929
|
-
const { config, watcher, queue } = deps;
|
|
12982
|
+
const { config, watcher, queue, cache } = deps;
|
|
12930
12983
|
if (body.path) {
|
|
12931
12984
|
// Path-targeted trigger: create override entry
|
|
12932
12985
|
const targetPath = resolveMetaDir(body.path);
|
|
@@ -12963,7 +13016,7 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
12963
13016
|
// Corpus-wide trigger: discover stalest candidate
|
|
12964
13017
|
let result;
|
|
12965
13018
|
try {
|
|
12966
|
-
result = await
|
|
13019
|
+
result = await cache.get(config, watcher);
|
|
12967
13020
|
}
|
|
12968
13021
|
catch {
|
|
12969
13022
|
return reply.status(503).send({
|
|
@@ -12971,20 +13024,15 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
12971
13024
|
message: 'Watcher unreachable — cannot discover candidates',
|
|
12972
13025
|
});
|
|
12973
13026
|
}
|
|
12974
|
-
const
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
node: e.node,
|
|
12978
|
-
meta: e.meta,
|
|
12979
|
-
actualStaleness: e.stalenessSeconds,
|
|
12980
|
-
}));
|
|
12981
|
-
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
12982
|
-
if (!stalest) {
|
|
13027
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
13028
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
13029
|
+
if (!winner) {
|
|
12983
13030
|
return reply.code(200).send({
|
|
12984
13031
|
status: 'skipped',
|
|
12985
13032
|
message: 'No stale metas found. Nothing to synthesize.',
|
|
12986
13033
|
});
|
|
12987
13034
|
}
|
|
13035
|
+
const stalest = winner.node.metaPath;
|
|
12988
13036
|
const enqueueResult = queue.enqueue(stalest);
|
|
12989
13037
|
return reply.code(202).send({
|
|
12990
13038
|
status: 'accepted',
|
|
@@ -13085,6 +13133,18 @@ function createServer(options) {
|
|
|
13085
13133
|
// Fastify 5 requires `loggerInstance` for external pino loggers
|
|
13086
13134
|
const app = Fastify({
|
|
13087
13135
|
loggerInstance: options.logger,
|
|
13136
|
+
requestTimeout: 30_000,
|
|
13137
|
+
});
|
|
13138
|
+
// Readiness gate: return 503 while service is initializing
|
|
13139
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
13140
|
+
if (options.deps.ready)
|
|
13141
|
+
return;
|
|
13142
|
+
const url = request.url;
|
|
13143
|
+
if (url === '/config' || url.startsWith('/config/apply'))
|
|
13144
|
+
return;
|
|
13145
|
+
return reply
|
|
13146
|
+
.status(503)
|
|
13147
|
+
.send({ status: 'starting', message: 'Service initializing' });
|
|
13088
13148
|
});
|
|
13089
13149
|
registerRoutes(app, options.deps);
|
|
13090
13150
|
return app;
|
|
@@ -13260,8 +13320,9 @@ async function startService(config, configPath) {
|
|
|
13260
13320
|
lastCycleAt: null,
|
|
13261
13321
|
};
|
|
13262
13322
|
const queue = new SynthesisQueue(logger);
|
|
13323
|
+
const cache = new MetaCache();
|
|
13263
13324
|
// Scheduler (needs watcher for discovery)
|
|
13264
|
-
const scheduler = new Scheduler(config, queue, logger, watcher);
|
|
13325
|
+
const scheduler = new Scheduler(config, queue, logger, watcher, cache);
|
|
13265
13326
|
const routeDeps = {
|
|
13266
13327
|
config,
|
|
13267
13328
|
logger,
|
|
@@ -13269,6 +13330,8 @@ async function startService(config, configPath) {
|
|
|
13269
13330
|
watcher,
|
|
13270
13331
|
scheduler,
|
|
13271
13332
|
stats,
|
|
13333
|
+
cache,
|
|
13334
|
+
ready: false,
|
|
13272
13335
|
executor,
|
|
13273
13336
|
configPath,
|
|
13274
13337
|
};
|
|
@@ -13319,6 +13382,9 @@ async function startService(config, configPath) {
|
|
|
13319
13382
|
}
|
|
13320
13383
|
await progress.report(evt);
|
|
13321
13384
|
}, logger);
|
|
13385
|
+
// Invalidate cache only when a phase was actually executed
|
|
13386
|
+
if (result.executed)
|
|
13387
|
+
cache.invalidate();
|
|
13322
13388
|
const durationMs = Date.now() - startMs;
|
|
13323
13389
|
if (!result.executed) {
|
|
13324
13390
|
logger.debug({ path: ownerPath }, 'Phase skipped (fully fresh or locked)');
|
|
@@ -13382,9 +13448,13 @@ async function startService(config, configPath) {
|
|
|
13382
13448
|
scheduler.setRegistrar(registrar);
|
|
13383
13449
|
routeDeps.registrar = registrar;
|
|
13384
13450
|
void registrar.register().then(() => {
|
|
13451
|
+
routeDeps.ready = true;
|
|
13385
13452
|
if (registrar.isRegistered) {
|
|
13386
13453
|
void verifyRuleApplication(watcher, logger);
|
|
13387
13454
|
}
|
|
13455
|
+
}, () => {
|
|
13456
|
+
// Registration failed after max retries — mark ready anyway
|
|
13457
|
+
routeDeps.ready = true;
|
|
13388
13458
|
});
|
|
13389
13459
|
// Periodic watcher health check (independent of scheduler)
|
|
13390
13460
|
const healthCheck = new WatcherHealthCheck({
|