@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
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs, { readdirSync, readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, mkdirSync, copyFileSync,
|
|
1
|
+
import fs, { readdirSync, readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, statSync, mkdirSync, copyFileSync, watchFile } from 'node:fs';
|
|
2
2
|
import path, { join, dirname, resolve, basename, relative, posix } from 'node:path';
|
|
3
3
|
import { unlink, readFile, mkdir, writeFile, copyFile } from 'node:fs/promises';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -16,8 +16,8 @@ import { z } from 'zod';
|
|
|
16
16
|
import * as commander from 'commander';
|
|
17
17
|
import 'node:child_process';
|
|
18
18
|
import { tmpdir } from 'node:os';
|
|
19
|
-
import pino from 'pino';
|
|
20
19
|
import Handlebars from 'handlebars';
|
|
20
|
+
import pino from 'pino';
|
|
21
21
|
import { Cron } from 'croner';
|
|
22
22
|
import Fastify from 'fastify';
|
|
23
23
|
|
|
@@ -7437,6 +7437,11 @@ const workspaceCoreConfigSchema = z
|
|
|
7437
7437
|
configRoot: z.string().optional().describe('Platform config root path'),
|
|
7438
7438
|
/** OpenClaw gateway URL. */
|
|
7439
7439
|
gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
|
|
7440
|
+
/** Dev repo paths keyed by component name. */
|
|
7441
|
+
devRepos: z
|
|
7442
|
+
.record(z.string(), z.string())
|
|
7443
|
+
.optional()
|
|
7444
|
+
.describe('Dev repo paths by component name'),
|
|
7440
7445
|
})
|
|
7441
7446
|
.partial();
|
|
7442
7447
|
/** Memory shared config section. */
|
|
@@ -7451,13 +7456,6 @@ const workspaceMemoryConfigSchema = z
|
|
|
7451
7456
|
.max(1)
|
|
7452
7457
|
.optional()
|
|
7453
7458
|
.describe('Memory warning threshold'),
|
|
7454
|
-
/** Staleness threshold in days. */
|
|
7455
|
-
staleDays: z
|
|
7456
|
-
.number()
|
|
7457
|
-
.int()
|
|
7458
|
-
.positive()
|
|
7459
|
-
.optional()
|
|
7460
|
-
.describe('Memory staleness threshold in days'),
|
|
7461
7459
|
})
|
|
7462
7460
|
.partial();
|
|
7463
7461
|
/** Workspace config Zod schema. */
|
|
@@ -7716,6 +7714,110 @@ async function postJson(url, body) {
|
|
|
7716
7714
|
});
|
|
7717
7715
|
}
|
|
7718
7716
|
|
|
7717
|
+
var toolsPlatformTemplate = `### Tool Hierarchy
|
|
7718
|
+
|
|
7719
|
+
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.
|
|
7720
|
+
|
|
7721
|
+
Use \`watcher_scan\` (no embeddings, no query string) for structural queries: file enumeration, staleness checks, domain listing, counts.
|
|
7722
|
+
|
|
7723
|
+
Direct filesystem access is for **acting on** search results, not bypassing them.
|
|
7724
|
+
|
|
7725
|
+
### Shell Scripting
|
|
7726
|
+
|
|
7727
|
+
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.
|
|
7728
|
+
|
|
7729
|
+
### File Bridge for External Repos
|
|
7730
|
+
|
|
7731
|
+
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.
|
|
7732
|
+
|
|
7733
|
+
### Gateway Self-Destruction Warning
|
|
7734
|
+
|
|
7735
|
+
⚠️ 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.
|
|
7736
|
+
|
|
7737
|
+
### Messaging
|
|
7738
|
+
|
|
7739
|
+
**Same-channel replies:** Don't use the \`message\` tool. It fires immediately, jumping ahead of streaming narration. Just write text as your response.
|
|
7740
|
+
|
|
7741
|
+
**Cross-channel sends:** Use the \`message\` tool with an explicit \`target\` to send to a different channel or DM.
|
|
7742
|
+
|
|
7743
|
+
### Plugin Lifecycle
|
|
7744
|
+
|
|
7745
|
+
\`\`\`bash
|
|
7746
|
+
# Platform bootstrap (content seeding)
|
|
7747
|
+
npx @karmaniverous/jeeves install
|
|
7748
|
+
|
|
7749
|
+
# Component plugin install
|
|
7750
|
+
npx @karmaniverous/jeeves-{component}-openclaw install
|
|
7751
|
+
|
|
7752
|
+
# Component plugin uninstall
|
|
7753
|
+
npx @karmaniverous/jeeves-{component}-openclaw uninstall
|
|
7754
|
+
|
|
7755
|
+
# Platform teardown (remove managed sections)
|
|
7756
|
+
npx @karmaniverous/jeeves uninstall
|
|
7757
|
+
\`\`\`
|
|
7758
|
+
|
|
7759
|
+
Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands above.
|
|
7760
|
+
|
|
7761
|
+
### Reference Templates
|
|
7762
|
+
|
|
7763
|
+
{{#if templatePath}}
|
|
7764
|
+
Reference templates are available at \`{{templatePath}}\`:
|
|
7765
|
+
|
|
7766
|
+
| Template | Purpose |
|
|
7767
|
+
|----------|---------|
|
|
7768
|
+
| \`spec.md\` | Skeleton for new product specifications — all section headers, decision format, dev plan format |
|
|
7769
|
+
| \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
|
|
7770
|
+
|
|
7771
|
+
Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
|
|
7772
|
+
{{else}}
|
|
7773
|
+
> Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
|
|
7774
|
+
{{/if}}
|
|
7775
|
+
|
|
7776
|
+
### Post-Upgrade Maintenance
|
|
7777
|
+
|
|
7778
|
+
After updating OpenClaw (\`npm install -g openclaw@latest\` or equivalent), reinstall all Jeeves component plugins to repair install state:
|
|
7779
|
+
|
|
7780
|
+
\`\`\`bash
|
|
7781
|
+
npx @karmaniverous/jeeves install
|
|
7782
|
+
npx @karmaniverous/jeeves-runner-openclaw install
|
|
7783
|
+
npx @karmaniverous/jeeves-watcher-openclaw install
|
|
7784
|
+
npx @karmaniverous/jeeves-server-openclaw install
|
|
7785
|
+
npx @karmaniverous/jeeves-meta-openclaw install
|
|
7786
|
+
\`\`\`
|
|
7787
|
+
|
|
7788
|
+
Then restart the gateway. Plugin installers copy dist files and patch config; reinstalling after an OpenClaw update ensures the extensions directory stays consistent.
|
|
7789
|
+
|
|
7790
|
+
### Source Code Preference
|
|
7791
|
+
|
|
7792
|
+
{{#if devRepos}}
|
|
7793
|
+
When investigating, debugging, or analyzing Jeeves components, always read TypeScript source from dev repos — never compiled \`dist/\` from the global npm install. Dev repos:
|
|
7794
|
+
|
|
7795
|
+
| Component | Dev Repo |
|
|
7796
|
+
|-----------|----------|
|
|
7797
|
+
{{#each devRepos}}
|
|
7798
|
+
| {{@key}} | \`{{this}}\` |
|
|
7799
|
+
{{/each}}
|
|
7800
|
+
|
|
7801
|
+
Built code is minified, harder to reason about, and wastes context. Always \`git pull\` before analysis.
|
|
7802
|
+
{{else}}
|
|
7803
|
+
> Dev repo paths not configured. Add \`core.devRepos\` to \`jeeves.config.json\` to enable source code preference guidance.
|
|
7804
|
+
{{/if}}
|
|
7805
|
+
`;
|
|
7806
|
+
|
|
7807
|
+
/**
|
|
7808
|
+
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
7809
|
+
*
|
|
7810
|
+
* @remarks
|
|
7811
|
+
* Called by `ComponentWriter` on each cycle. Not directly exposed to components.
|
|
7812
|
+
* Reads content files from the package's `content/` directory, renders the
|
|
7813
|
+
* Platform template with live data, and writes managed sections using
|
|
7814
|
+
* `updateManagedSection`.
|
|
7815
|
+
*/
|
|
7816
|
+
/** Compiled Handlebars template for the Platform section (cached at module level). */
|
|
7817
|
+
Handlebars.compile(toolsPlatformTemplate, {
|
|
7818
|
+
noEscape: true,
|
|
7819
|
+
});
|
|
7820
|
+
|
|
7719
7821
|
/**
|
|
7720
7822
|
* Core configuration schema and resolution.
|
|
7721
7823
|
*
|
|
@@ -7812,10 +7914,10 @@ function getServiceUrl(serviceName, consumerName) {
|
|
|
7812
7914
|
if (coreUrl)
|
|
7813
7915
|
return coreUrl;
|
|
7814
7916
|
// 3. Fall back to port constants
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
return `http://127.0.0.1:${String(port)}`;
|
|
7917
|
+
if (serviceName in DEFAULT_PORTS) {
|
|
7918
|
+
return `http://127.0.0.1:${String(DEFAULT_PORTS[serviceName])}`;
|
|
7818
7919
|
}
|
|
7920
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
7819
7921
|
}
|
|
7820
7922
|
|
|
7821
7923
|
/**
|
|
@@ -8014,355 +8116,94 @@ function registerCustomCliCommands(program) {
|
|
|
8014
8116
|
}
|
|
8015
8117
|
|
|
8016
8118
|
/**
|
|
8017
|
-
*
|
|
8119
|
+
* Compute summary statistics from an array of MetaEntry objects.
|
|
8018
8120
|
*
|
|
8019
|
-
*
|
|
8020
|
-
* via the component descriptor's onConfigApply callback.
|
|
8121
|
+
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
8021
8122
|
*
|
|
8022
|
-
* @module
|
|
8123
|
+
* @module discovery/computeSummary
|
|
8023
8124
|
*/
|
|
8024
8125
|
/**
|
|
8025
|
-
*
|
|
8126
|
+
* Compute summary statistics from a list of meta entries.
|
|
8026
8127
|
*
|
|
8027
|
-
*
|
|
8028
|
-
*
|
|
8128
|
+
* @param entries - Enriched meta entries (full or filtered).
|
|
8129
|
+
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
8130
|
+
* @returns Aggregated summary statistics.
|
|
8029
8131
|
*/
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
let
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
config.logging.level = newConfig.logging.level;
|
|
8063
|
-
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
8064
|
-
}
|
|
8065
|
-
const restartSet = new Set(RESTART_REQUIRED_FIELDS);
|
|
8066
|
-
for (const key of Object.keys(newConfig)) {
|
|
8067
|
-
if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
|
|
8068
|
-
continue;
|
|
8132
|
+
function computeSummary(entries, depthWeight) {
|
|
8133
|
+
let staleCount = 0;
|
|
8134
|
+
let errorCount = 0;
|
|
8135
|
+
let lockedCount = 0;
|
|
8136
|
+
let disabledCount = 0;
|
|
8137
|
+
let neverSynthesizedCount = 0;
|
|
8138
|
+
let totalArchitectTokens = 0;
|
|
8139
|
+
let totalBuilderTokens = 0;
|
|
8140
|
+
let totalCriticTokens = 0;
|
|
8141
|
+
let stalestPath = null;
|
|
8142
|
+
let stalestEffective = -1;
|
|
8143
|
+
let lastSynthesizedPath = null;
|
|
8144
|
+
let lastSynthesizedAt = null;
|
|
8145
|
+
for (const e of entries) {
|
|
8146
|
+
if (e.stalenessSeconds > 0)
|
|
8147
|
+
staleCount++;
|
|
8148
|
+
if (e.hasError)
|
|
8149
|
+
errorCount++;
|
|
8150
|
+
if (e.locked)
|
|
8151
|
+
lockedCount++;
|
|
8152
|
+
if (e.disabled)
|
|
8153
|
+
disabledCount++;
|
|
8154
|
+
if (e.lastSynthesized === null)
|
|
8155
|
+
neverSynthesizedCount++;
|
|
8156
|
+
totalArchitectTokens += e.architectTokens ?? 0;
|
|
8157
|
+
totalBuilderTokens += e.builderTokens ?? 0;
|
|
8158
|
+
totalCriticTokens += e.criticTokens ?? 0;
|
|
8159
|
+
// Track last synthesized
|
|
8160
|
+
if (e.lastSynthesized &&
|
|
8161
|
+
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
8162
|
+
lastSynthesizedAt = e.lastSynthesized;
|
|
8163
|
+
lastSynthesizedPath = e.path;
|
|
8069
8164
|
}
|
|
8070
|
-
|
|
8071
|
-
const
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8165
|
+
// Track stalest (effective staleness for scheduling)
|
|
8166
|
+
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
8167
|
+
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
8168
|
+
if (effectiveStaleness > stalestEffective) {
|
|
8169
|
+
stalestEffective = effectiveStaleness;
|
|
8170
|
+
stalestPath = e.path;
|
|
8075
8171
|
}
|
|
8076
8172
|
}
|
|
8173
|
+
return {
|
|
8174
|
+
total: entries.length,
|
|
8175
|
+
stale: staleCount,
|
|
8176
|
+
errors: errorCount,
|
|
8177
|
+
locked: lockedCount,
|
|
8178
|
+
disabled: disabledCount,
|
|
8179
|
+
neverSynthesized: neverSynthesizedCount,
|
|
8180
|
+
tokens: {
|
|
8181
|
+
architect: totalArchitectTokens,
|
|
8182
|
+
builder: totalBuilderTokens,
|
|
8183
|
+
critic: totalCriticTokens,
|
|
8184
|
+
},
|
|
8185
|
+
stalestPath,
|
|
8186
|
+
lastSynthesizedPath,
|
|
8187
|
+
lastSynthesizedAt,
|
|
8188
|
+
};
|
|
8077
8189
|
}
|
|
8078
8190
|
|
|
8079
8191
|
/**
|
|
8080
|
-
*
|
|
8192
|
+
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
8081
8193
|
*
|
|
8082
|
-
*
|
|
8194
|
+
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
8195
|
+
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
8083
8196
|
*
|
|
8084
|
-
* @module
|
|
8197
|
+
* @module discovery/discoverMetas
|
|
8085
8198
|
*/
|
|
8086
|
-
/** Zod schema for the core (library-compatible) meta configuration. */
|
|
8087
|
-
const metaConfigSchema = z.object({
|
|
8088
|
-
/** Watcher service base URL. */
|
|
8089
|
-
watcherUrl: z.url(),
|
|
8090
|
-
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
8091
|
-
gatewayUrl: z.url().default('http://127.0.0.1:18789'),
|
|
8092
|
-
/** Optional API key for gateway authentication. */
|
|
8093
|
-
gatewayApiKey: z.string().optional(),
|
|
8094
|
-
/** Run architect every N cycles (per meta). */
|
|
8095
|
-
architectEvery: z.number().int().min(1).default(10),
|
|
8096
|
-
/** Exponent for depth weighting in staleness formula. */
|
|
8097
|
-
depthWeight: z.number().min(0).default(0.5),
|
|
8098
|
-
/** Maximum archive snapshots to retain per meta. */
|
|
8099
|
-
maxArchive: z.number().int().min(1).default(20),
|
|
8100
|
-
/** Maximum lines of context to include in subprocess prompts. */
|
|
8101
|
-
maxLines: z.number().int().min(50).default(500),
|
|
8102
|
-
/** Architect subprocess timeout in seconds. */
|
|
8103
|
-
architectTimeout: z.number().int().min(30).default(180),
|
|
8104
|
-
/** Builder subprocess timeout in seconds. */
|
|
8105
|
-
builderTimeout: z.number().int().min(60).default(360),
|
|
8106
|
-
/** Critic subprocess timeout in seconds. */
|
|
8107
|
-
criticTimeout: z.number().int().min(30).default(240),
|
|
8108
|
-
/** Thinking level for spawned synthesis sessions. */
|
|
8109
|
-
thinking: z.string().default('low'),
|
|
8110
|
-
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
8111
|
-
defaultArchitect: z.string().optional(),
|
|
8112
|
-
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
8113
|
-
defaultCritic: z.string().optional(),
|
|
8114
|
-
/** Skip unchanged candidates, bump _generatedAt. */
|
|
8115
|
-
skipUnchanged: z.boolean().default(true),
|
|
8116
|
-
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
8117
|
-
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
8118
|
-
/** Watcher metadata properties applied to archive snapshots. */
|
|
8119
|
-
metaArchiveProperty: z
|
|
8120
|
-
.record(z.string(), z.unknown())
|
|
8121
|
-
.default({ _meta: 'archive' }),
|
|
8122
|
-
});
|
|
8123
|
-
/** Zod schema for logging configuration. */
|
|
8124
|
-
const loggingSchema = z.object({
|
|
8125
|
-
/** Log level. */
|
|
8126
|
-
level: z.string().default('info'),
|
|
8127
|
-
/** Optional file path for log output. */
|
|
8128
|
-
file: z.string().optional(),
|
|
8129
|
-
});
|
|
8130
|
-
/** Zod schema for a single auto-seed policy rule. */
|
|
8131
|
-
const autoSeedRuleSchema = z.object({
|
|
8132
|
-
/** Glob pattern matched against watcher walk results. */
|
|
8133
|
-
match: z.string(),
|
|
8134
|
-
/** Optional steering prompt for seeded metas. */
|
|
8135
|
-
steer: z.string().optional(),
|
|
8136
|
-
/** Optional cross-references for seeded metas. */
|
|
8137
|
-
crossRefs: z.array(z.string()).optional(),
|
|
8138
|
-
});
|
|
8139
|
-
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
8140
|
-
const serviceConfigSchema = metaConfigSchema.extend({
|
|
8141
|
-
/** HTTP port for the service (default: 1938). */
|
|
8142
|
-
port: z.number().int().min(1).max(65535).default(1938),
|
|
8143
|
-
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
8144
|
-
schedule: z.string().default('*/30 * * * *'),
|
|
8145
|
-
/** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
|
|
8146
|
-
reportChannel: z.string().optional(),
|
|
8147
|
-
/** Channel/user ID to send progress messages to. */
|
|
8148
|
-
reportTarget: z.string().optional(),
|
|
8149
|
-
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
8150
|
-
serverBaseUrl: z.string().optional(),
|
|
8151
|
-
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
8152
|
-
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
8153
|
-
/** Logging configuration. */
|
|
8154
|
-
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
8155
|
-
/**
|
|
8156
|
-
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
8157
|
-
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
8158
|
-
*/
|
|
8159
|
-
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
8160
|
-
});
|
|
8161
|
-
|
|
8162
8199
|
/**
|
|
8163
|
-
*
|
|
8164
|
-
*
|
|
8165
|
-
* Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
|
|
8200
|
+
* Discover all .meta/ directories via watcher walk.
|
|
8166
8201
|
*
|
|
8167
|
-
*
|
|
8168
|
-
|
|
8169
|
-
/**
|
|
8170
|
-
* Deep-walk a value, replacing `\${VAR\}` patterns with process.env values.
|
|
8202
|
+
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
8203
|
+
* and returns deduplicated meta directory paths.
|
|
8171
8204
|
*
|
|
8172
|
-
* @param
|
|
8173
|
-
* @returns
|
|
8174
|
-
*/
|
|
8175
|
-
function substituteEnvVars(value) {
|
|
8176
|
-
if (typeof value === 'string') {
|
|
8177
|
-
return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
|
|
8178
|
-
const envVal = process.env[name];
|
|
8179
|
-
if (envVal === undefined) {
|
|
8180
|
-
throw new Error(`Environment variable ${name} is not set`);
|
|
8181
|
-
}
|
|
8182
|
-
return envVal;
|
|
8183
|
-
});
|
|
8184
|
-
}
|
|
8185
|
-
if (Array.isArray(value)) {
|
|
8186
|
-
return value.map(substituteEnvVars);
|
|
8187
|
-
}
|
|
8188
|
-
if (value !== null && typeof value === 'object') {
|
|
8189
|
-
const result = {};
|
|
8190
|
-
for (const [key, val] of Object.entries(value)) {
|
|
8191
|
-
result[key] = substituteEnvVars(val);
|
|
8192
|
-
}
|
|
8193
|
-
return result;
|
|
8194
|
-
}
|
|
8195
|
-
return value;
|
|
8196
|
-
}
|
|
8197
|
-
/**
|
|
8198
|
-
* Resolve \@file: references in a config value.
|
|
8199
|
-
*
|
|
8200
|
-
* @param value - String value that may start with "\@file:".
|
|
8201
|
-
* @param baseDir - Base directory for resolving relative paths.
|
|
8202
|
-
* @returns The resolved string (file contents or original value).
|
|
8203
|
-
*/
|
|
8204
|
-
function resolveFileRef(value, baseDir) {
|
|
8205
|
-
if (!value.startsWith('@file:'))
|
|
8206
|
-
return value;
|
|
8207
|
-
const filePath = join(baseDir, value.slice(6));
|
|
8208
|
-
return readFileSync(filePath, 'utf8');
|
|
8209
|
-
}
|
|
8210
|
-
/**
|
|
8211
|
-
* Migrate legacy config path to the new canonical location.
|
|
8212
|
-
*
|
|
8213
|
-
* If the old path `{configRoot}/jeeves-meta.config.json` exists and the new
|
|
8214
|
-
* path `{configRoot}/jeeves-meta/config.json` does NOT exist, copies the file
|
|
8215
|
-
* to the new location and logs a warning.
|
|
8216
|
-
*
|
|
8217
|
-
* @param configRoot - Root directory for configuration files.
|
|
8218
|
-
* @param warn - Optional callback for logging the migration warning.
|
|
8219
|
-
*/
|
|
8220
|
-
function migrateConfigPath(configRoot, warn) {
|
|
8221
|
-
const oldPath = join(configRoot, 'jeeves-meta.config.json');
|
|
8222
|
-
const newDir = join(configRoot, 'jeeves-meta');
|
|
8223
|
-
const newPath = join(newDir, 'config.json');
|
|
8224
|
-
if (existsSync(oldPath) && !existsSync(newPath)) {
|
|
8225
|
-
mkdirSync(newDir, { recursive: true });
|
|
8226
|
-
copyFileSync(oldPath, newPath);
|
|
8227
|
-
const message = `Migrated config from ${oldPath} to ${newPath}. The old file can be removed.`;
|
|
8228
|
-
if (warn) {
|
|
8229
|
-
warn(message);
|
|
8230
|
-
}
|
|
8231
|
-
else {
|
|
8232
|
-
console.warn(`[jeeves-meta] ${message}`);
|
|
8233
|
-
}
|
|
8234
|
-
}
|
|
8235
|
-
}
|
|
8236
|
-
/**
|
|
8237
|
-
* Resolve config path from --config flag or JEEVES_META_CONFIG env var.
|
|
8238
|
-
*
|
|
8239
|
-
* @param args - CLI arguments (process.argv.slice(2)).
|
|
8240
|
-
* @returns Resolved config path.
|
|
8241
|
-
* @throws If no config path found.
|
|
8242
|
-
*/
|
|
8243
|
-
function resolveConfigPath(args) {
|
|
8244
|
-
let configIdx = args.indexOf('--config');
|
|
8245
|
-
if (configIdx === -1)
|
|
8246
|
-
configIdx = args.indexOf('-c');
|
|
8247
|
-
if (configIdx !== -1 && args[configIdx + 1]) {
|
|
8248
|
-
return args[configIdx + 1];
|
|
8249
|
-
}
|
|
8250
|
-
const envPath = process.env['JEEVES_META_CONFIG'];
|
|
8251
|
-
if (envPath)
|
|
8252
|
-
return envPath;
|
|
8253
|
-
throw new Error('Config path required. Use --config <path> or set JEEVES_META_CONFIG env var.');
|
|
8254
|
-
}
|
|
8255
|
-
/**
|
|
8256
|
-
* Load service config from a JSON file.
|
|
8257
|
-
*
|
|
8258
|
-
* Resolves \@file: references for defaultArchitect and defaultCritic,
|
|
8259
|
-
* and substitutes environment-variable placeholders throughout.
|
|
8260
|
-
*
|
|
8261
|
-
* @param configPath - Path to config JSON file.
|
|
8262
|
-
* @returns Validated ServiceConfig.
|
|
8263
|
-
*/
|
|
8264
|
-
function loadServiceConfig(configPath) {
|
|
8265
|
-
const rawText = readFileSync(configPath, 'utf8');
|
|
8266
|
-
const raw = substituteEnvVars(JSON.parse(rawText));
|
|
8267
|
-
const baseDir = dirname(configPath);
|
|
8268
|
-
if (typeof raw['defaultArchitect'] === 'string') {
|
|
8269
|
-
raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
|
|
8270
|
-
}
|
|
8271
|
-
if (typeof raw['defaultCritic'] === 'string') {
|
|
8272
|
-
raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
|
|
8273
|
-
}
|
|
8274
|
-
return serviceConfigSchema.parse(raw);
|
|
8275
|
-
}
|
|
8276
|
-
|
|
8277
|
-
/**
|
|
8278
|
-
* Compute summary statistics from an array of MetaEntry objects.
|
|
8279
|
-
*
|
|
8280
|
-
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
8281
|
-
*
|
|
8282
|
-
* @module discovery/computeSummary
|
|
8283
|
-
*/
|
|
8284
|
-
/**
|
|
8285
|
-
* Compute summary statistics from a list of meta entries.
|
|
8286
|
-
*
|
|
8287
|
-
* @param entries - Enriched meta entries (full or filtered).
|
|
8288
|
-
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
8289
|
-
* @returns Aggregated summary statistics.
|
|
8290
|
-
*/
|
|
8291
|
-
function computeSummary(entries, depthWeight) {
|
|
8292
|
-
let staleCount = 0;
|
|
8293
|
-
let errorCount = 0;
|
|
8294
|
-
let lockedCount = 0;
|
|
8295
|
-
let disabledCount = 0;
|
|
8296
|
-
let neverSynthesizedCount = 0;
|
|
8297
|
-
let totalArchitectTokens = 0;
|
|
8298
|
-
let totalBuilderTokens = 0;
|
|
8299
|
-
let totalCriticTokens = 0;
|
|
8300
|
-
let stalestPath = null;
|
|
8301
|
-
let stalestEffective = -1;
|
|
8302
|
-
let lastSynthesizedPath = null;
|
|
8303
|
-
let lastSynthesizedAt = null;
|
|
8304
|
-
for (const e of entries) {
|
|
8305
|
-
if (e.stalenessSeconds > 0)
|
|
8306
|
-
staleCount++;
|
|
8307
|
-
if (e.hasError)
|
|
8308
|
-
errorCount++;
|
|
8309
|
-
if (e.locked)
|
|
8310
|
-
lockedCount++;
|
|
8311
|
-
if (e.disabled)
|
|
8312
|
-
disabledCount++;
|
|
8313
|
-
if (e.lastSynthesized === null)
|
|
8314
|
-
neverSynthesizedCount++;
|
|
8315
|
-
totalArchitectTokens += e.architectTokens ?? 0;
|
|
8316
|
-
totalBuilderTokens += e.builderTokens ?? 0;
|
|
8317
|
-
totalCriticTokens += e.criticTokens ?? 0;
|
|
8318
|
-
// Track last synthesized
|
|
8319
|
-
if (e.lastSynthesized &&
|
|
8320
|
-
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
8321
|
-
lastSynthesizedAt = e.lastSynthesized;
|
|
8322
|
-
lastSynthesizedPath = e.path;
|
|
8323
|
-
}
|
|
8324
|
-
// Track stalest (effective staleness for scheduling)
|
|
8325
|
-
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
8326
|
-
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
8327
|
-
if (effectiveStaleness > stalestEffective) {
|
|
8328
|
-
stalestEffective = effectiveStaleness;
|
|
8329
|
-
stalestPath = e.path;
|
|
8330
|
-
}
|
|
8331
|
-
}
|
|
8332
|
-
return {
|
|
8333
|
-
total: entries.length,
|
|
8334
|
-
stale: staleCount,
|
|
8335
|
-
errors: errorCount,
|
|
8336
|
-
locked: lockedCount,
|
|
8337
|
-
disabled: disabledCount,
|
|
8338
|
-
neverSynthesized: neverSynthesizedCount,
|
|
8339
|
-
tokens: {
|
|
8340
|
-
architect: totalArchitectTokens,
|
|
8341
|
-
builder: totalBuilderTokens,
|
|
8342
|
-
critic: totalCriticTokens,
|
|
8343
|
-
},
|
|
8344
|
-
stalestPath,
|
|
8345
|
-
lastSynthesizedPath,
|
|
8346
|
-
lastSynthesizedAt,
|
|
8347
|
-
};
|
|
8348
|
-
}
|
|
8349
|
-
|
|
8350
|
-
/**
|
|
8351
|
-
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
8352
|
-
*
|
|
8353
|
-
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
8354
|
-
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
8355
|
-
*
|
|
8356
|
-
* @module discovery/discoverMetas
|
|
8357
|
-
*/
|
|
8358
|
-
/**
|
|
8359
|
-
* Discover all .meta/ directories via watcher walk.
|
|
8360
|
-
*
|
|
8361
|
-
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
8362
|
-
* and returns deduplicated meta directory paths.
|
|
8363
|
-
*
|
|
8364
|
-
* @param watcher - WatcherClient for walk queries.
|
|
8365
|
-
* @returns Array of normalized .meta/ directory paths.
|
|
8205
|
+
* @param watcher - WatcherClient for walk queries.
|
|
8206
|
+
* @returns Array of normalized .meta/ directory paths.
|
|
8366
8207
|
*/
|
|
8367
8208
|
async function discoverMetas(watcher) {
|
|
8368
8209
|
const allPaths = await watcher.walk(['**/.meta/meta.json']);
|
|
@@ -8928,1483 +8769,928 @@ function getDeltaFiles(generatedAt, scopeFiles) {
|
|
|
8928
8769
|
}
|
|
8929
8770
|
|
|
8930
8771
|
/**
|
|
8931
|
-
*
|
|
8772
|
+
* In-memory cache for listMetas results with TTL and concurrent refresh guard.
|
|
8932
8773
|
*
|
|
8933
|
-
* @module
|
|
8774
|
+
* @module cache
|
|
8934
8775
|
*/
|
|
8935
|
-
|
|
8936
|
-
class SpawnAbortedError extends Error {
|
|
8937
|
-
constructor(message = 'Synthesis was aborted') {
|
|
8938
|
-
super(message);
|
|
8939
|
-
this.name = 'SpawnAbortedError';
|
|
8940
|
-
}
|
|
8941
|
-
}
|
|
8942
|
-
|
|
8776
|
+
const TTL_MS = 60_000;
|
|
8943
8777
|
/**
|
|
8944
|
-
*
|
|
8945
|
-
*
|
|
8946
|
-
* Carries the output file path so callers can attempt partial output recovery.
|
|
8947
|
-
*
|
|
8948
|
-
* @module executor/SpawnTimeoutError
|
|
8778
|
+
* Caches listMetas results to avoid expensive repeated filesystem walks.
|
|
8779
|
+
* Supports concurrent refresh coalescing and manual invalidation.
|
|
8949
8780
|
*/
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
this.
|
|
8957
|
-
|
|
8781
|
+
class MetaCache {
|
|
8782
|
+
result = null;
|
|
8783
|
+
updatedAt = 0;
|
|
8784
|
+
refreshPromise = null;
|
|
8785
|
+
/** Get cached result or refresh if stale. */
|
|
8786
|
+
async get(config, watcher) {
|
|
8787
|
+
if (this.result && Date.now() - this.updatedAt < TTL_MS) {
|
|
8788
|
+
return this.result;
|
|
8789
|
+
}
|
|
8790
|
+
return this.refresh(config, watcher);
|
|
8791
|
+
}
|
|
8792
|
+
/** Force-expire the cache so next get() triggers a refresh. */
|
|
8793
|
+
invalidate() {
|
|
8794
|
+
this.updatedAt = 0;
|
|
8795
|
+
}
|
|
8796
|
+
async refresh(config, watcher) {
|
|
8797
|
+
if (this.refreshPromise)
|
|
8798
|
+
return this.refreshPromise;
|
|
8799
|
+
this.refreshPromise = listMetas(config, watcher)
|
|
8800
|
+
.then((result) => {
|
|
8801
|
+
this.result = result;
|
|
8802
|
+
this.updatedAt = Date.now();
|
|
8803
|
+
return result;
|
|
8804
|
+
})
|
|
8805
|
+
.finally(() => {
|
|
8806
|
+
this.refreshPromise = null;
|
|
8807
|
+
});
|
|
8808
|
+
return this.refreshPromise;
|
|
8958
8809
|
}
|
|
8959
8810
|
}
|
|
8960
8811
|
|
|
8961
8812
|
/**
|
|
8962
|
-
*
|
|
8813
|
+
* Shared live config hot-reload support.
|
|
8963
8814
|
*
|
|
8964
|
-
*
|
|
8965
|
-
*
|
|
8966
|
-
* polls for completion, and extracts output text.
|
|
8815
|
+
* Used by both file-watch reloads in bootstrap and POST /config/apply
|
|
8816
|
+
* via the component descriptor's onConfigApply callback.
|
|
8967
8817
|
*
|
|
8968
|
-
* @module
|
|
8818
|
+
* @module configHotReload
|
|
8969
8819
|
*/
|
|
8970
|
-
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
8971
|
-
const DEFAULT_TIMEOUT_MS$1 = 600_000; // 10 minutes
|
|
8972
8820
|
/**
|
|
8973
|
-
*
|
|
8974
|
-
* `/tools/invoke` endpoint.
|
|
8821
|
+
* Fields that require a service restart to take effect.
|
|
8975
8822
|
*
|
|
8976
|
-
*
|
|
8977
|
-
*
|
|
8978
|
-
* optional `apiKey` — typically sourced from `MetaConfig`.
|
|
8823
|
+
* Shared between the descriptor's `onConfigApply` and the file-watcher
|
|
8824
|
+
* hot-reload in `bootstrap.ts`.
|
|
8979
8825
|
*/
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
async invoke(tool, args, sessionKey) {
|
|
9004
|
-
const headers = {
|
|
9005
|
-
'Content-Type': 'application/json',
|
|
9006
|
-
};
|
|
9007
|
-
if (this.apiKey) {
|
|
9008
|
-
headers['Authorization'] = 'Bearer ' + this.apiKey;
|
|
9009
|
-
}
|
|
9010
|
-
const body = { tool, args };
|
|
9011
|
-
if (sessionKey)
|
|
9012
|
-
body.sessionKey = sessionKey;
|
|
9013
|
-
const res = await fetch(this.gatewayUrl + '/tools/invoke', {
|
|
9014
|
-
method: 'POST',
|
|
9015
|
-
headers,
|
|
9016
|
-
body: JSON.stringify(body),
|
|
9017
|
-
});
|
|
9018
|
-
if (!res.ok) {
|
|
9019
|
-
const text = await res.text();
|
|
9020
|
-
throw new Error(`Gateway ${tool} failed: HTTP ${res.status.toString()} - ${text}`);
|
|
9021
|
-
}
|
|
9022
|
-
const data = (await res.json());
|
|
9023
|
-
if (data.ok === false || data.error) {
|
|
9024
|
-
throw new Error(`Gateway ${tool} error: ${data.error?.message ?? JSON.stringify(data)}`);
|
|
9025
|
-
}
|
|
9026
|
-
return data;
|
|
9027
|
-
}
|
|
9028
|
-
/** Look up totalTokens for a session via sessions_list. */
|
|
9029
|
-
async getSessionTokens(sessionKey) {
|
|
9030
|
-
try {
|
|
9031
|
-
const result = await this.invoke('sessions_list', {
|
|
9032
|
-
limit: 20,
|
|
9033
|
-
messageLimit: 0,
|
|
9034
|
-
});
|
|
9035
|
-
const sessions = (result.result?.details?.sessions ??
|
|
9036
|
-
result.result?.sessions ??
|
|
9037
|
-
[]);
|
|
9038
|
-
const match = sessions.find((s) => s.key === sessionKey);
|
|
9039
|
-
return match?.totalTokens ?? undefined;
|
|
9040
|
-
}
|
|
9041
|
-
catch {
|
|
9042
|
-
return undefined;
|
|
8826
|
+
const RESTART_REQUIRED_FIELDS = [
|
|
8827
|
+
'port',
|
|
8828
|
+
'watcherUrl',
|
|
8829
|
+
'gatewayUrl',
|
|
8830
|
+
'gatewayApiKey',
|
|
8831
|
+
'defaultArchitect',
|
|
8832
|
+
'defaultCritic',
|
|
8833
|
+
];
|
|
8834
|
+
let runtime = null;
|
|
8835
|
+
/** Register the active service runtime for config-apply hot reload. */
|
|
8836
|
+
function registerConfigHotReloadRuntime(nextRuntime) {
|
|
8837
|
+
runtime = nextRuntime;
|
|
8838
|
+
}
|
|
8839
|
+
/** Apply hot-reloadable config changes to the live shared config object. */
|
|
8840
|
+
function applyHotReloadedConfig(newConfig) {
|
|
8841
|
+
if (!runtime)
|
|
8842
|
+
return;
|
|
8843
|
+
const { config, logger, scheduler } = runtime;
|
|
8844
|
+
for (const field of RESTART_REQUIRED_FIELDS) {
|
|
8845
|
+
const oldVal = config[field];
|
|
8846
|
+
const nextVal = newConfig[field];
|
|
8847
|
+
if (oldVal !== nextVal) {
|
|
8848
|
+
logger.warn({ field, oldValue: oldVal, newValue: nextVal }, 'Config field changed but requires restart to take effect');
|
|
9043
8849
|
}
|
|
9044
8850
|
}
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
8851
|
+
if (newConfig.schedule !== config.schedule) {
|
|
8852
|
+
scheduler?.updateSchedule(newConfig.schedule);
|
|
8853
|
+
config.schedule = newConfig.schedule;
|
|
8854
|
+
logger.info({ schedule: newConfig.schedule }, 'Schedule hot-reloaded');
|
|
9048
8855
|
}
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
8856
|
+
if (newConfig.logging.level !== config.logging.level) {
|
|
8857
|
+
logger.level = newConfig.logging.level;
|
|
8858
|
+
config.logging.level = newConfig.logging.level;
|
|
8859
|
+
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
9052
8860
|
}
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
const timeoutMs = timeoutSeconds * 1000;
|
|
9058
|
-
const deadline = Date.now() + timeoutMs;
|
|
9059
|
-
// Ensure workspace dir exists
|
|
9060
|
-
if (!existsSync(this.workspaceDir)) {
|
|
9061
|
-
mkdirSync(this.workspaceDir, { recursive: true });
|
|
9062
|
-
}
|
|
9063
|
-
// Generate unique output path for file-based output
|
|
9064
|
-
const outputId = randomUUID();
|
|
9065
|
-
const outputPath = this.workspaceDir + '/output-' + outputId + '.json';
|
|
9066
|
-
// Append file output instruction to the task
|
|
9067
|
-
const taskWithOutput = task +
|
|
9068
|
-
'\n\n## OUTPUT DELIVERY\n\n' +
|
|
9069
|
-
'Write your complete output to a file using the Write tool at:\n' +
|
|
9070
|
-
outputPath +
|
|
9071
|
-
'\n\n' +
|
|
9072
|
-
'Reply with ONLY the file path you wrote to. No other text.';
|
|
9073
|
-
// Step 1: Spawn the sub-agent session (unique label per cycle to avoid
|
|
9074
|
-
// "label already in use" errors — gateway labels persist after session completion)
|
|
9075
|
-
const labelBase = options?.label ?? 'jeeves-meta-synthesis';
|
|
9076
|
-
const label = labelBase + '-' + outputId.slice(0, 8);
|
|
9077
|
-
const spawnResult = await this.invoke('sessions_spawn', {
|
|
9078
|
-
task: taskWithOutput,
|
|
9079
|
-
label,
|
|
9080
|
-
runTimeoutSeconds: timeoutSeconds,
|
|
9081
|
-
...(options?.thinking ? { thinking: options.thinking } : {}),
|
|
9082
|
-
...(options?.model ? { model: options.model } : {}),
|
|
9083
|
-
});
|
|
9084
|
-
const details = (spawnResult.result?.details ?? spawnResult.result);
|
|
9085
|
-
const sessionKey = details?.childSessionKey ?? details?.sessionKey;
|
|
9086
|
-
if (typeof sessionKey !== 'string' || !sessionKey) {
|
|
9087
|
-
throw new Error('Gateway sessions_spawn returned no sessionKey: ' +
|
|
9088
|
-
JSON.stringify(spawnResult));
|
|
8861
|
+
const restartSet = new Set(RESTART_REQUIRED_FIELDS);
|
|
8862
|
+
for (const key of Object.keys(newConfig)) {
|
|
8863
|
+
if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
|
|
8864
|
+
continue;
|
|
9089
8865
|
}
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
this.cleanupOutputFile(outputPath);
|
|
9096
|
-
throw new SpawnAbortedError();
|
|
9097
|
-
}
|
|
9098
|
-
try {
|
|
9099
|
-
const historyResult = await this.invoke('sessions_history', {
|
|
9100
|
-
sessionKey,
|
|
9101
|
-
limit: 5,
|
|
9102
|
-
includeTools: false,
|
|
9103
|
-
});
|
|
9104
|
-
const messages = historyResult.result?.details?.messages ??
|
|
9105
|
-
historyResult.result?.messages ??
|
|
9106
|
-
[];
|
|
9107
|
-
const msgArray = messages;
|
|
9108
|
-
if (msgArray.length > 0) {
|
|
9109
|
-
const lastMsg = msgArray[msgArray.length - 1];
|
|
9110
|
-
// Complete when last message is assistant with a terminal stop reason
|
|
9111
|
-
if (lastMsg.role === 'assistant' &&
|
|
9112
|
-
lastMsg.stopReason &&
|
|
9113
|
-
lastMsg.stopReason !== 'toolUse' &&
|
|
9114
|
-
lastMsg.stopReason !== 'error') {
|
|
9115
|
-
// Fetch token usage from session metadata
|
|
9116
|
-
const tokens = await this.getSessionTokens(sessionKey);
|
|
9117
|
-
// Read output from file (sub-agent wrote it via Write tool)
|
|
9118
|
-
if (existsSync(outputPath)) {
|
|
9119
|
-
try {
|
|
9120
|
-
const output = readFileSync(outputPath, 'utf8');
|
|
9121
|
-
return { output, tokens };
|
|
9122
|
-
}
|
|
9123
|
-
finally {
|
|
9124
|
-
try {
|
|
9125
|
-
unlinkSync(outputPath);
|
|
9126
|
-
}
|
|
9127
|
-
catch {
|
|
9128
|
-
/* cleanup best-effort */
|
|
9129
|
-
}
|
|
9130
|
-
}
|
|
9131
|
-
}
|
|
9132
|
-
// Fallback: extract from message content if file wasn't written
|
|
9133
|
-
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9134
|
-
const msg = msgArray[i];
|
|
9135
|
-
if (msg.role === 'assistant' && msg.content) {
|
|
9136
|
-
const text = typeof msg.content === 'string'
|
|
9137
|
-
? msg.content
|
|
9138
|
-
: Array.isArray(msg.content)
|
|
9139
|
-
? msg.content
|
|
9140
|
-
.filter((b) => b.type === 'text' && b.text)
|
|
9141
|
-
.map((b) => b.text)
|
|
9142
|
-
.join('\n')
|
|
9143
|
-
: '';
|
|
9144
|
-
if (text)
|
|
9145
|
-
return { output: text, tokens };
|
|
9146
|
-
}
|
|
9147
|
-
}
|
|
9148
|
-
return { output: '', tokens };
|
|
9149
|
-
}
|
|
9150
|
-
}
|
|
9151
|
-
}
|
|
9152
|
-
catch {
|
|
9153
|
-
// Transient poll failure — keep trying
|
|
9154
|
-
}
|
|
9155
|
-
await sleepAsync(this.pollIntervalMs);
|
|
8866
|
+
const oldVal = config[key];
|
|
8867
|
+
const nextVal = newConfig[key];
|
|
8868
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(nextVal)) {
|
|
8869
|
+
config[key] = nextVal;
|
|
8870
|
+
logger.info({ field: key }, 'Config field hot-reloaded');
|
|
9156
8871
|
}
|
|
9157
|
-
throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
|
|
9158
8872
|
}
|
|
9159
8873
|
}
|
|
9160
8874
|
|
|
9161
8875
|
/**
|
|
9162
|
-
*
|
|
8876
|
+
* Zod schema for jeeves-meta service configuration.
|
|
9163
8877
|
*
|
|
9164
|
-
*
|
|
9165
|
-
*/
|
|
9166
|
-
/**
|
|
9167
|
-
* Create a pino logger instance.
|
|
8878
|
+
* The service config is a strict superset of the core (library-compatible) meta config.
|
|
9168
8879
|
*
|
|
9169
|
-
* @
|
|
9170
|
-
* @returns Configured pino logger.
|
|
8880
|
+
* @module schema/config
|
|
9171
8881
|
*/
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
8882
|
+
/** Zod schema for the core (library-compatible) meta configuration. */
|
|
8883
|
+
const metaConfigSchema = z.object({
|
|
8884
|
+
/** Watcher service base URL. */
|
|
8885
|
+
watcherUrl: z.url(),
|
|
8886
|
+
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
8887
|
+
gatewayUrl: z.url().default('http://127.0.0.1:18789'),
|
|
8888
|
+
/** Optional API key for gateway authentication. */
|
|
8889
|
+
gatewayApiKey: z.string().optional(),
|
|
8890
|
+
/** Run architect every N cycles (per meta). */
|
|
8891
|
+
architectEvery: z.number().int().min(1).default(10),
|
|
8892
|
+
/** Exponent for depth weighting in staleness formula. */
|
|
8893
|
+
depthWeight: z.number().min(0).default(0.5),
|
|
8894
|
+
/** Maximum archive snapshots to retain per meta. */
|
|
8895
|
+
maxArchive: z.number().int().min(1).default(20),
|
|
8896
|
+
/** Maximum lines of context to include in subprocess prompts. */
|
|
8897
|
+
maxLines: z.number().int().min(50).default(500),
|
|
8898
|
+
/** Architect subprocess timeout in seconds. */
|
|
8899
|
+
architectTimeout: z.number().int().min(30).default(180),
|
|
8900
|
+
/** Builder subprocess timeout in seconds. */
|
|
8901
|
+
builderTimeout: z.number().int().min(60).default(360),
|
|
8902
|
+
/** Critic subprocess timeout in seconds. */
|
|
8903
|
+
criticTimeout: z.number().int().min(30).default(240),
|
|
8904
|
+
/** Thinking level for spawned synthesis sessions. */
|
|
8905
|
+
thinking: z.string().default('low'),
|
|
8906
|
+
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
8907
|
+
defaultArchitect: z.string().optional(),
|
|
8908
|
+
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
8909
|
+
defaultCritic: z.string().optional(),
|
|
8910
|
+
/** Skip unchanged candidates, bump _generatedAt. */
|
|
8911
|
+
skipUnchanged: z.boolean().default(true),
|
|
8912
|
+
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
8913
|
+
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
8914
|
+
/** Watcher metadata properties applied to archive snapshots. */
|
|
8915
|
+
metaArchiveProperty: z
|
|
8916
|
+
.record(z.string(), z.unknown())
|
|
8917
|
+
.default({ _meta: 'archive' }),
|
|
8918
|
+
});
|
|
8919
|
+
/** Zod schema for logging configuration. */
|
|
8920
|
+
const loggingSchema = z.object({
|
|
8921
|
+
/** Log level. */
|
|
8922
|
+
level: z.string().default('info'),
|
|
8923
|
+
/** Optional file path for log output. */
|
|
8924
|
+
file: z.string().optional(),
|
|
8925
|
+
});
|
|
8926
|
+
/** Zod schema for a single auto-seed policy rule. */
|
|
8927
|
+
const autoSeedRuleSchema = z.object({
|
|
8928
|
+
/** Glob pattern matched against watcher walk results. */
|
|
8929
|
+
match: z.string(),
|
|
8930
|
+
/** Optional steering prompt for seeded metas. */
|
|
8931
|
+
steer: z.string().optional(),
|
|
8932
|
+
/** Optional cross-references for seeded metas. */
|
|
8933
|
+
crossRefs: z.array(z.string()).optional(),
|
|
8934
|
+
});
|
|
8935
|
+
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
8936
|
+
const serviceConfigSchema = metaConfigSchema.extend({
|
|
8937
|
+
/** HTTP port for the service (default: 1938). */
|
|
8938
|
+
port: z.number().int().min(1).max(65535).default(1938),
|
|
8939
|
+
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
8940
|
+
schedule: z.string().default('*/30 * * * *'),
|
|
8941
|
+
/** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
|
|
8942
|
+
reportChannel: z.string().optional(),
|
|
8943
|
+
/** Channel/user ID to send progress messages to. */
|
|
8944
|
+
reportTarget: z.string().optional(),
|
|
8945
|
+
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
8946
|
+
serverBaseUrl: z.string().optional(),
|
|
8947
|
+
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
8948
|
+
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
8949
|
+
/** Logging configuration. */
|
|
8950
|
+
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
8951
|
+
/**
|
|
8952
|
+
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
8953
|
+
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
8954
|
+
*/
|
|
8955
|
+
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
8956
|
+
});
|
|
9183
8957
|
|
|
9184
8958
|
/**
|
|
9185
|
-
*
|
|
8959
|
+
* Load and resolve jeeves-meta service config.
|
|
9186
8960
|
*
|
|
9187
|
-
*
|
|
9188
|
-
* Loaded at runtime relative to the compiled module location.
|
|
9189
|
-
*
|
|
9190
|
-
* Users can override via `defaultArchitect` / `defaultCritic` in the service
|
|
9191
|
-
* config. Most installations should use the built-in defaults.
|
|
9192
|
-
*
|
|
9193
|
-
* @module prompts
|
|
9194
|
-
*/
|
|
9195
|
-
const packageRoot = packageDirectorySync({
|
|
9196
|
-
cwd: fileURLToPath(import.meta.url),
|
|
9197
|
-
});
|
|
9198
|
-
const promptDir = join(packageRoot, 'dist', 'prompts');
|
|
9199
|
-
/** Built-in default architect prompt. */
|
|
9200
|
-
const DEFAULT_ARCHITECT_PROMPT = readFileSync(join(promptDir, 'architect.md'), 'utf8');
|
|
9201
|
-
/** Built-in default critic prompt. */
|
|
9202
|
-
const DEFAULT_CRITIC_PROMPT = readFileSync(join(promptDir, 'critic.md'), 'utf8');
|
|
9203
|
-
|
|
9204
|
-
/**
|
|
9205
|
-
* Build the MetaContext for a synthesis cycle.
|
|
9206
|
-
*
|
|
9207
|
-
* Computes shared inputs once: scope files, delta files, child meta outputs,
|
|
9208
|
-
* previous content/feedback, steer, and archive paths.
|
|
9209
|
-
*
|
|
9210
|
-
* @module orchestrator/contextPackage
|
|
9211
|
-
*/
|
|
9212
|
-
/**
|
|
9213
|
-
* Condense a file list into glob-like summaries.
|
|
9214
|
-
* Groups by directory + extension pattern.
|
|
9215
|
-
*
|
|
9216
|
-
* @param files - Array of file paths.
|
|
9217
|
-
* @param maxIndividual - Show individual files up to this count.
|
|
9218
|
-
* @returns Condensed summary string.
|
|
9219
|
-
*/
|
|
9220
|
-
function condenseScopeFiles(files, maxIndividual = 30) {
|
|
9221
|
-
if (files.length <= maxIndividual)
|
|
9222
|
-
return files.join('\n');
|
|
9223
|
-
// Group by dir + extension
|
|
9224
|
-
const groups = new Map();
|
|
9225
|
-
for (const f of files) {
|
|
9226
|
-
const dir = f.substring(0, f.lastIndexOf('/') + 1) || './';
|
|
9227
|
-
const ext = f.includes('.') ? f.substring(f.lastIndexOf('.')) : '(no ext)';
|
|
9228
|
-
const key = dir + '*' + ext;
|
|
9229
|
-
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
9230
|
-
}
|
|
9231
|
-
// Sort by count descending
|
|
9232
|
-
const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
9233
|
-
return sorted
|
|
9234
|
-
.map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
|
|
9235
|
-
.join('\n');
|
|
9236
|
-
}
|
|
9237
|
-
/**
|
|
9238
|
-
* Read a meta.json file and extract its `_content` field.
|
|
9239
|
-
*
|
|
9240
|
-
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
9241
|
-
* @returns The `_content` string, or null if missing/unreadable.
|
|
9242
|
-
*/
|
|
9243
|
-
async function readMetaContent(metaJsonPath) {
|
|
9244
|
-
try {
|
|
9245
|
-
const raw = await readFile(metaJsonPath, 'utf8');
|
|
9246
|
-
const meta = JSON.parse(raw);
|
|
9247
|
-
return meta._content ?? null;
|
|
9248
|
-
}
|
|
9249
|
-
catch {
|
|
9250
|
-
return null;
|
|
9251
|
-
}
|
|
9252
|
-
}
|
|
9253
|
-
/**
|
|
9254
|
-
* Build the context package for a synthesis cycle.
|
|
9255
|
-
*
|
|
9256
|
-
* @param node - The meta node being synthesized.
|
|
9257
|
-
* @param meta - Current meta.json content.
|
|
9258
|
-
* @param watcher - WatcherClient for scope enumeration.
|
|
9259
|
-
* @returns The computed context package.
|
|
9260
|
-
*/
|
|
9261
|
-
async function buildContextPackage(node, meta, watcher, logger) {
|
|
9262
|
-
// Scope and delta files via watcher walk
|
|
9263
|
-
const scopeStart = Date.now();
|
|
9264
|
-
const { scopeFiles } = await getScopeFiles(node, watcher, logger);
|
|
9265
|
-
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
9266
|
-
logger?.debug({
|
|
9267
|
-
scopeFiles: scopeFiles.length,
|
|
9268
|
-
deltaFiles: deltaFiles.length,
|
|
9269
|
-
durationMs: Date.now() - scopeStart,
|
|
9270
|
-
}, 'scope and delta files computed');
|
|
9271
|
-
// Child meta outputs (parallel reads)
|
|
9272
|
-
const childMetas = {};
|
|
9273
|
-
const childEntries = await Promise.all(node.children.map(async (child) => {
|
|
9274
|
-
const content = await readMetaContent(join(child.metaPath, 'meta.json'));
|
|
9275
|
-
return [child.ownerPath, content];
|
|
9276
|
-
}));
|
|
9277
|
-
for (const [path, content] of childEntries) {
|
|
9278
|
-
childMetas[path] = content;
|
|
9279
|
-
}
|
|
9280
|
-
// Cross-referenced meta outputs (parallel reads)
|
|
9281
|
-
const crossRefMetas = {};
|
|
9282
|
-
const seen = new Set();
|
|
9283
|
-
const crossRefPaths = [];
|
|
9284
|
-
for (const refPath of meta._crossRefs ?? []) {
|
|
9285
|
-
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
9286
|
-
continue;
|
|
9287
|
-
if (seen.has(refPath))
|
|
9288
|
-
continue;
|
|
9289
|
-
seen.add(refPath);
|
|
9290
|
-
crossRefPaths.push(refPath);
|
|
9291
|
-
}
|
|
9292
|
-
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
9293
|
-
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
9294
|
-
return [refPath, content];
|
|
9295
|
-
}));
|
|
9296
|
-
for (const [path, content] of crossRefEntries) {
|
|
9297
|
-
crossRefMetas[path] = content;
|
|
9298
|
-
}
|
|
9299
|
-
// Archive paths
|
|
9300
|
-
const archives = listArchiveFiles(node.metaPath);
|
|
9301
|
-
return {
|
|
9302
|
-
path: node.metaPath,
|
|
9303
|
-
scopeFiles,
|
|
9304
|
-
deltaFiles,
|
|
9305
|
-
childMetas,
|
|
9306
|
-
crossRefMetas,
|
|
9307
|
-
previousContent: meta._content ?? null,
|
|
9308
|
-
previousFeedback: meta._feedback ?? null,
|
|
9309
|
-
steer: meta._steer ?? null,
|
|
9310
|
-
previousState: meta._state ?? null,
|
|
9311
|
-
archives,
|
|
9312
|
-
};
|
|
9313
|
-
}
|
|
9314
|
-
|
|
9315
|
-
/**
|
|
9316
|
-
* Build task prompts for each synthesis step.
|
|
9317
|
-
*
|
|
9318
|
-
* Prompts are compiled as Handlebars templates with access to config,
|
|
9319
|
-
* meta, and scope context. The architect can write template expressions
|
|
9320
|
-
* into its _builder output; these resolve when the builder task is compiled.
|
|
8961
|
+
* Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
|
|
9321
8962
|
*
|
|
9322
|
-
* @module
|
|
8963
|
+
* @module configLoader
|
|
9323
8964
|
*/
|
|
9324
|
-
/** Build the template context from synthesis inputs. */
|
|
9325
|
-
function buildTemplateContext(ctx, meta, config) {
|
|
9326
|
-
return {
|
|
9327
|
-
config,
|
|
9328
|
-
meta,
|
|
9329
|
-
scope: {
|
|
9330
|
-
fileCount: ctx.scopeFiles.length,
|
|
9331
|
-
deltaCount: ctx.deltaFiles.length,
|
|
9332
|
-
childCount: Object.keys(ctx.childMetas).length,
|
|
9333
|
-
crossRefCount: Object.keys(ctx.crossRefMetas).length,
|
|
9334
|
-
},
|
|
9335
|
-
};
|
|
9336
|
-
}
|
|
9337
8965
|
/**
|
|
9338
|
-
*
|
|
9339
|
-
* Returns the original string unchanged if compilation fails.
|
|
9340
|
-
*/
|
|
9341
|
-
function compileTemplate(text, context) {
|
|
9342
|
-
try {
|
|
9343
|
-
return Handlebars.compile(text, { noEscape: true })(context);
|
|
9344
|
-
}
|
|
9345
|
-
catch {
|
|
9346
|
-
return text;
|
|
9347
|
-
}
|
|
9348
|
-
}
|
|
9349
|
-
/** Append a keyed record of meta outputs as subsections, if non-empty. */
|
|
9350
|
-
function appendMetaSections(sections, heading, metas) {
|
|
9351
|
-
if (Object.keys(metas).length === 0)
|
|
9352
|
-
return;
|
|
9353
|
-
sections.push('', heading);
|
|
9354
|
-
for (const [path, content] of Object.entries(metas)) {
|
|
9355
|
-
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
9356
|
-
}
|
|
9357
|
-
}
|
|
9358
|
-
/** Append optional context sections shared across all step prompts. */
|
|
9359
|
-
function appendSharedSections(sections, ctx, options) {
|
|
9360
|
-
const opts = {
|
|
9361
|
-
includeSteer: true,
|
|
9362
|
-
includePreviousContent: true,
|
|
9363
|
-
includePreviousFeedback: true,
|
|
9364
|
-
feedbackHeading: '## PREVIOUS FEEDBACK',
|
|
9365
|
-
includeChildMetas: true,
|
|
9366
|
-
includeCrossRefs: true,
|
|
9367
|
-
...options,
|
|
9368
|
-
};
|
|
9369
|
-
if (opts.includeSteer && ctx.steer) {
|
|
9370
|
-
sections.push('', '## STEERING PROMPT', ctx.steer);
|
|
9371
|
-
}
|
|
9372
|
-
if (opts.includePreviousContent && ctx.previousContent) {
|
|
9373
|
-
sections.push('', '## PREVIOUS SYNTHESIS', ctx.previousContent);
|
|
9374
|
-
}
|
|
9375
|
-
if (opts.includePreviousFeedback && ctx.previousFeedback) {
|
|
9376
|
-
sections.push('', opts.feedbackHeading, ctx.previousFeedback);
|
|
9377
|
-
}
|
|
9378
|
-
if (opts.includeChildMetas) {
|
|
9379
|
-
appendMetaSections(sections, '## CHILD META OUTPUTS', ctx.childMetas);
|
|
9380
|
-
}
|
|
9381
|
-
if (opts.includeCrossRefs) {
|
|
9382
|
-
appendMetaSections(sections, '## CROSS-REFERENCED METAS', ctx.crossRefMetas);
|
|
9383
|
-
}
|
|
9384
|
-
}
|
|
9385
|
-
/**
|
|
9386
|
-
* Build the architect task prompt.
|
|
8966
|
+
* Deep-walk a value, replacing `\${VAR\}` patterns with process.env values.
|
|
9387
8967
|
*
|
|
9388
|
-
* @param
|
|
9389
|
-
* @
|
|
9390
|
-
* @param config - Synthesis config.
|
|
9391
|
-
* @returns The architect task prompt string.
|
|
8968
|
+
* @param value - Arbitrary JSON-compatible value.
|
|
8969
|
+
* @returns Value with env-var placeholders resolved.
|
|
9392
8970
|
*/
|
|
9393
|
-
function
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
`Files changed since last synthesis: ${ctx.deltaFiles.length.toString()}`,
|
|
9403
|
-
'',
|
|
9404
|
-
'### File listing (scope)',
|
|
9405
|
-
condenseScopeFiles(ctx.scopeFiles),
|
|
9406
|
-
];
|
|
9407
|
-
// Inject previous _builder so architect can see its own prior output
|
|
9408
|
-
if (meta._builder) {
|
|
9409
|
-
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
8971
|
+
function substituteEnvVars(value) {
|
|
8972
|
+
if (typeof value === 'string') {
|
|
8973
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
|
|
8974
|
+
const envVal = process.env[name];
|
|
8975
|
+
if (envVal === undefined) {
|
|
8976
|
+
throw new Error(`Environment variable ${name} is not set`);
|
|
8977
|
+
}
|
|
8978
|
+
return envVal;
|
|
8979
|
+
});
|
|
9410
8980
|
}
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
sections.push('', '## ARCHIVE HISTORY', `${ctx.archives.length.toString()} previous synthesis snapshots available in .meta/archive/.`, 'Review these to understand how the synthesis has evolved over time.');
|
|
8981
|
+
if (Array.isArray(value)) {
|
|
8982
|
+
return value.map(substituteEnvVars);
|
|
9414
8983
|
}
|
|
9415
|
-
|
|
9416
|
-
}
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
* @param meta - Current meta.json.
|
|
9422
|
-
* @param config - Synthesis config.
|
|
9423
|
-
* @returns The builder task prompt string.
|
|
9424
|
-
*/
|
|
9425
|
-
function buildBuilderTask(ctx, meta, config) {
|
|
9426
|
-
const sections = [
|
|
9427
|
-
`# jeeves-meta · BUILDER · ${ctx.path}`,
|
|
9428
|
-
'',
|
|
9429
|
-
'## TASK BRIEF (from Architect)',
|
|
9430
|
-
meta._builder ?? '(No architect brief available)',
|
|
9431
|
-
'',
|
|
9432
|
-
'## SCOPE',
|
|
9433
|
-
`Path: ${ctx.path}`,
|
|
9434
|
-
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
9435
|
-
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
9436
|
-
];
|
|
9437
|
-
if (ctx.previousState != null) {
|
|
9438
|
-
sections.push('', '## PREVIOUS STATE', 'The following opaque state was returned by the previous synthesis cycle.', 'Use it to continue progressive work. Update `_state` in your output to', 'reflect your progress.', '', '```json', JSON.stringify(ctx.previousState, null, 2), '```');
|
|
8984
|
+
if (value !== null && typeof value === 'object') {
|
|
8985
|
+
const result = {};
|
|
8986
|
+
for (const [key, val] of Object.entries(value)) {
|
|
8987
|
+
result[key] = substituteEnvVars(val);
|
|
8988
|
+
}
|
|
8989
|
+
return result;
|
|
9439
8990
|
}
|
|
9440
|
-
|
|
9441
|
-
includeSteer: false,
|
|
9442
|
-
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
9443
|
-
});
|
|
9444
|
-
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.');
|
|
9445
|
-
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
9446
|
-
}
|
|
9447
|
-
/**
|
|
9448
|
-
* Build the critic task prompt.
|
|
9449
|
-
*
|
|
9450
|
-
* @param ctx - Synthesis context.
|
|
9451
|
-
* @param meta - Current meta.json (with _content already set by builder).
|
|
9452
|
-
* @param config - Synthesis config.
|
|
9453
|
-
* @returns The critic task prompt string.
|
|
9454
|
-
*/
|
|
9455
|
-
function buildCriticTask(ctx, meta, config) {
|
|
9456
|
-
const sections = [
|
|
9457
|
-
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
9458
|
-
'',
|
|
9459
|
-
meta._critic ?? config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
9460
|
-
'',
|
|
9461
|
-
'## SYNTHESIS TO EVALUATE',
|
|
9462
|
-
meta._content ?? '(No content produced)',
|
|
9463
|
-
'',
|
|
9464
|
-
'## SCOPE',
|
|
9465
|
-
`Path: ${ctx.path}`,
|
|
9466
|
-
`Files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
9467
|
-
];
|
|
9468
|
-
appendSharedSections(sections, ctx, {
|
|
9469
|
-
includePreviousContent: false,
|
|
9470
|
-
feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
|
|
9471
|
-
includeChildMetas: false,
|
|
9472
|
-
includeCrossRefs: false,
|
|
9473
|
-
});
|
|
9474
|
-
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
9475
|
-
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
8991
|
+
return value;
|
|
9476
8992
|
}
|
|
9477
|
-
|
|
9478
8993
|
/**
|
|
9479
|
-
*
|
|
9480
|
-
*
|
|
9481
|
-
* @module ema
|
|
9482
|
-
*/
|
|
9483
|
-
const DEFAULT_DECAY = 0.3;
|
|
9484
|
-
/**
|
|
9485
|
-
* Compute exponential moving average.
|
|
8994
|
+
* Resolve \@file: references in a config value.
|
|
9486
8995
|
*
|
|
9487
|
-
* @param
|
|
9488
|
-
* @param
|
|
9489
|
-
* @
|
|
9490
|
-
* @returns Updated EMA.
|
|
8996
|
+
* @param value - String value that may start with "\@file:".
|
|
8997
|
+
* @param baseDir - Base directory for resolving relative paths.
|
|
8998
|
+
* @returns The resolved string (file contents or original value).
|
|
9491
8999
|
*/
|
|
9492
|
-
function
|
|
9493
|
-
if (
|
|
9494
|
-
return
|
|
9495
|
-
|
|
9000
|
+
function resolveFileRef(value, baseDir) {
|
|
9001
|
+
if (!value.startsWith('@file:'))
|
|
9002
|
+
return value;
|
|
9003
|
+
const filePath = join(baseDir, value.slice(6));
|
|
9004
|
+
return readFileSync(filePath, 'utf8');
|
|
9496
9005
|
}
|
|
9497
|
-
|
|
9498
|
-
/**
|
|
9499
|
-
* Structured error from a synthesis step failure.
|
|
9500
|
-
*
|
|
9501
|
-
* @module schema/error
|
|
9502
|
-
*/
|
|
9503
|
-
/** Zod schema for synthesis step errors. */
|
|
9504
|
-
const metaErrorSchema = z.object({
|
|
9505
|
-
/** Which step failed: 'architect', 'builder', or 'critic'. */
|
|
9506
|
-
step: z.enum(['architect', 'builder', 'critic']),
|
|
9507
|
-
/** Error classification code. */
|
|
9508
|
-
code: z.string(),
|
|
9509
|
-
/** Human-readable error message. */
|
|
9510
|
-
message: z.string(),
|
|
9511
|
-
});
|
|
9512
|
-
|
|
9513
9006
|
/**
|
|
9514
|
-
*
|
|
9007
|
+
* Migrate legacy config path to the new canonical location.
|
|
9515
9008
|
*
|
|
9516
|
-
*
|
|
9517
|
-
*
|
|
9009
|
+
* If the old path `{configRoot}/jeeves-meta.config.json` exists and the new
|
|
9010
|
+
* path `{configRoot}/jeeves-meta/config.json` does NOT exist, copies the file
|
|
9011
|
+
* to the new location and logs a warning.
|
|
9518
9012
|
*
|
|
9519
|
-
* @
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
'
|
|
9524
|
-
'
|
|
9525
|
-
'
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
}
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
_critic: z.string().optional(),
|
|
9558
|
-
/** Timestamp of last synthesis. ISO 8601. */
|
|
9559
|
-
_generatedAt: z.iso.datetime().optional(),
|
|
9560
|
-
/** Narrative synthesis output. Rendered by watcher for embedding. */
|
|
9561
|
-
_content: z.string().optional(),
|
|
9562
|
-
/**
|
|
9563
|
-
* Hash of sorted file listing in scope. Detects directory structure
|
|
9564
|
-
* changes that trigger an architect re-run.
|
|
9565
|
-
*/
|
|
9566
|
-
_structureHash: z.string().optional(),
|
|
9567
|
-
/**
|
|
9568
|
-
* Cycles since last architect run. Reset to 0 when architect runs.
|
|
9569
|
-
* Used with architectEvery to trigger periodic re-prompting.
|
|
9570
|
-
*/
|
|
9571
|
-
_synthesisCount: z.number().int().min(0).optional(),
|
|
9572
|
-
/** Critic evaluation of the last synthesis. */
|
|
9573
|
-
_feedback: z.string().optional(),
|
|
9574
|
-
/**
|
|
9575
|
-
* Present and true on archive snapshots. Distinguishes live vs. archived
|
|
9576
|
-
* metas.
|
|
9577
|
-
*/
|
|
9578
|
-
_archived: z.boolean().optional(),
|
|
9579
|
-
/** Timestamp when this snapshot was archived. ISO 8601. */
|
|
9580
|
-
_archivedAt: z.iso.datetime().optional(),
|
|
9581
|
-
/**
|
|
9582
|
-
* Scheduling priority. Higher = updates more often. Negative allowed;
|
|
9583
|
-
* normalized to min 0 at scheduling time.
|
|
9584
|
-
*/
|
|
9585
|
-
_depth: z.number().optional(),
|
|
9586
|
-
/**
|
|
9587
|
-
* Emphasis multiplier for depth weighting in scheduling.
|
|
9588
|
-
* Default 1. Higher values increase this meta's scheduling priority
|
|
9589
|
-
* relative to its depth. Set to 0.5 to halve the depth effect,
|
|
9590
|
-
* 2 to double it, 0 to ignore depth entirely for this meta.
|
|
9591
|
-
*/
|
|
9592
|
-
_emphasis: z.number().min(0).optional(),
|
|
9593
|
-
/** Token count from last architect subprocess call. */
|
|
9594
|
-
_architectTokens: z.number().int().optional(),
|
|
9595
|
-
/** Token count from last builder subprocess call. */
|
|
9596
|
-
_builderTokens: z.number().int().optional(),
|
|
9597
|
-
/** Token count from last critic subprocess call. */
|
|
9598
|
-
_criticTokens: z.number().int().optional(),
|
|
9599
|
-
/** Exponential moving average of architect token usage (decay 0.3). */
|
|
9600
|
-
_architectTokensAvg: z.number().optional(),
|
|
9601
|
-
/** Exponential moving average of builder token usage (decay 0.3). */
|
|
9602
|
-
_builderTokensAvg: z.number().optional(),
|
|
9603
|
-
/** Exponential moving average of critic token usage (decay 0.3). */
|
|
9604
|
-
_criticTokensAvg: z.number().optional(),
|
|
9605
|
-
/**
|
|
9606
|
-
* Opaque state carried across synthesis cycles for progressive work.
|
|
9607
|
-
* Set by the builder, passed back as context on next cycle.
|
|
9608
|
-
*/
|
|
9609
|
-
_state: z.unknown().optional(),
|
|
9610
|
-
/**
|
|
9611
|
-
* Structured error from last cycle. Present when a step failed.
|
|
9612
|
-
* Cleared on successful cycle.
|
|
9613
|
-
*/
|
|
9614
|
-
_error: metaErrorSchema.optional(),
|
|
9615
|
-
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
9616
|
-
_disabled: z.boolean().optional(),
|
|
9617
|
-
/**
|
|
9618
|
-
* Per-phase state machine record. Engine-managed.
|
|
9619
|
-
* Keyed by phase name (architect, builder, critic) with status values.
|
|
9620
|
-
* Persisted to survive ticks; derived on first load for back-compat.
|
|
9621
|
-
*/
|
|
9622
|
-
_phaseState: phaseStateSchema.optional(),
|
|
9623
|
-
})
|
|
9624
|
-
.loose();
|
|
9625
|
-
|
|
9013
|
+
* @param configRoot - Root directory for configuration files.
|
|
9014
|
+
* @param warn - Optional callback for logging the migration warning.
|
|
9015
|
+
*/
|
|
9016
|
+
function migrateConfigPath(configRoot, warn) {
|
|
9017
|
+
const oldPath = join(configRoot, 'jeeves-meta.config.json');
|
|
9018
|
+
const newDir = join(configRoot, 'jeeves-meta');
|
|
9019
|
+
const newPath = join(newDir, 'config.json');
|
|
9020
|
+
if (existsSync(oldPath) && !existsSync(newPath)) {
|
|
9021
|
+
mkdirSync(newDir, { recursive: true });
|
|
9022
|
+
copyFileSync(oldPath, newPath);
|
|
9023
|
+
const message = `Migrated config from ${oldPath} to ${newPath}. The old file can be removed.`;
|
|
9024
|
+
if (warn) {
|
|
9025
|
+
warn(message);
|
|
9026
|
+
}
|
|
9027
|
+
else {
|
|
9028
|
+
console.warn(`[jeeves-meta] ${message}`);
|
|
9029
|
+
}
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
9032
|
+
/**
|
|
9033
|
+
* Resolve config path from --config flag or JEEVES_META_CONFIG env var.
|
|
9034
|
+
*
|
|
9035
|
+
* @param args - CLI arguments (process.argv.slice(2)).
|
|
9036
|
+
* @returns Resolved config path.
|
|
9037
|
+
* @throws If no config path found.
|
|
9038
|
+
*/
|
|
9039
|
+
function resolveConfigPath(args) {
|
|
9040
|
+
let configIdx = args.indexOf('--config');
|
|
9041
|
+
if (configIdx === -1)
|
|
9042
|
+
configIdx = args.indexOf('-c');
|
|
9043
|
+
if (configIdx !== -1 && args[configIdx + 1]) {
|
|
9044
|
+
return args[configIdx + 1];
|
|
9045
|
+
}
|
|
9046
|
+
const envPath = process.env['JEEVES_META_CONFIG'];
|
|
9047
|
+
if (envPath)
|
|
9048
|
+
return envPath;
|
|
9049
|
+
throw new Error('Config path required. Use --config <path> or set JEEVES_META_CONFIG env var.');
|
|
9050
|
+
}
|
|
9626
9051
|
/**
|
|
9627
|
-
*
|
|
9052
|
+
* Load service config from a JSON file.
|
|
9628
9053
|
*
|
|
9629
|
-
*
|
|
9630
|
-
*
|
|
9631
|
-
* Validates against schema before writing.
|
|
9054
|
+
* Resolves \@file: references for defaultArchitect and defaultCritic,
|
|
9055
|
+
* and substitutes environment-variable placeholders throughout.
|
|
9632
9056
|
*
|
|
9633
|
-
* @
|
|
9057
|
+
* @param configPath - Path to config JSON file.
|
|
9058
|
+
* @returns Validated ServiceConfig.
|
|
9634
9059
|
*/
|
|
9060
|
+
function loadServiceConfig(configPath) {
|
|
9061
|
+
const rawText = readFileSync(configPath, 'utf8');
|
|
9062
|
+
const raw = substituteEnvVars(JSON.parse(rawText));
|
|
9063
|
+
const baseDir = dirname(configPath);
|
|
9064
|
+
if (typeof raw['defaultArchitect'] === 'string') {
|
|
9065
|
+
raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
|
|
9066
|
+
}
|
|
9067
|
+
if (typeof raw['defaultCritic'] === 'string') {
|
|
9068
|
+
raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
|
|
9069
|
+
}
|
|
9070
|
+
return serviceConfigSchema.parse(raw);
|
|
9071
|
+
}
|
|
9072
|
+
|
|
9635
9073
|
/**
|
|
9636
|
-
*
|
|
9074
|
+
* Error thrown when a spawned subprocess is aborted via AbortController.
|
|
9637
9075
|
*
|
|
9638
|
-
* @
|
|
9639
|
-
* @returns The updated MetaJson.
|
|
9640
|
-
* @throws If validation fails (malformed output).
|
|
9076
|
+
* @module executor/SpawnAbortedError
|
|
9641
9077
|
*/
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
_depth: options.current._depth,
|
|
9648
|
-
_emphasis: options.current._emphasis,
|
|
9649
|
-
// Engine fields
|
|
9650
|
-
_architect: options.architect,
|
|
9651
|
-
_builder: options.builder,
|
|
9652
|
-
_critic: options.critic,
|
|
9653
|
-
_generatedAt: options.stateOnly
|
|
9654
|
-
? options.current._generatedAt
|
|
9655
|
-
: new Date().toISOString(),
|
|
9656
|
-
_structureHash: options.structureHash,
|
|
9657
|
-
_synthesisCount: options.synthesisCount,
|
|
9658
|
-
// Token tracking
|
|
9659
|
-
_architectTokens: options.architectTokens,
|
|
9660
|
-
_builderTokens: options.builderTokens,
|
|
9661
|
-
_criticTokens: options.criticTokens,
|
|
9662
|
-
_architectTokensAvg: options.architectTokens !== undefined
|
|
9663
|
-
? computeEma(options.architectTokens, options.current._architectTokensAvg)
|
|
9664
|
-
: options.current._architectTokensAvg,
|
|
9665
|
-
_builderTokensAvg: options.builderTokens !== undefined
|
|
9666
|
-
? computeEma(options.builderTokens, options.current._builderTokensAvg)
|
|
9667
|
-
: options.current._builderTokensAvg,
|
|
9668
|
-
_criticTokensAvg: options.criticTokens !== undefined
|
|
9669
|
-
? computeEma(options.criticTokens, options.current._criticTokensAvg)
|
|
9670
|
-
: options.current._criticTokensAvg,
|
|
9671
|
-
// Content from builder (stateOnly preserves previous content)
|
|
9672
|
-
_content: options.stateOnly
|
|
9673
|
-
? options.current._content
|
|
9674
|
-
: (options.builderOutput?.content ?? options.current._content),
|
|
9675
|
-
// Feedback from critic
|
|
9676
|
-
_feedback: options.feedback ?? options.current._feedback,
|
|
9677
|
-
// Progressive state
|
|
9678
|
-
_state: options.state,
|
|
9679
|
-
// Error handling
|
|
9680
|
-
_error: options.error ?? undefined,
|
|
9681
|
-
// Phase state machine
|
|
9682
|
-
_phaseState: options.phaseState,
|
|
9683
|
-
// Spread structured fields from builder
|
|
9684
|
-
...options.builderOutput?.fields,
|
|
9685
|
-
};
|
|
9686
|
-
// Clean up undefined optional fields
|
|
9687
|
-
if (merged._steer === undefined)
|
|
9688
|
-
delete merged._steer;
|
|
9689
|
-
if (merged._depth === undefined)
|
|
9690
|
-
delete merged._depth;
|
|
9691
|
-
if (merged._emphasis === undefined)
|
|
9692
|
-
delete merged._emphasis;
|
|
9693
|
-
if (merged._architectTokens === undefined)
|
|
9694
|
-
delete merged._architectTokens;
|
|
9695
|
-
if (merged._builderTokens === undefined)
|
|
9696
|
-
delete merged._builderTokens;
|
|
9697
|
-
if (merged._criticTokens === undefined)
|
|
9698
|
-
delete merged._criticTokens;
|
|
9699
|
-
if (merged._architectTokensAvg === undefined)
|
|
9700
|
-
delete merged._architectTokensAvg;
|
|
9701
|
-
if (merged._builderTokensAvg === undefined)
|
|
9702
|
-
delete merged._builderTokensAvg;
|
|
9703
|
-
if (merged._criticTokensAvg === undefined)
|
|
9704
|
-
delete merged._criticTokensAvg;
|
|
9705
|
-
if (merged._state === undefined)
|
|
9706
|
-
delete merged._state;
|
|
9707
|
-
if (merged._error === undefined)
|
|
9708
|
-
delete merged._error;
|
|
9709
|
-
if (merged._content === undefined)
|
|
9710
|
-
delete merged._content;
|
|
9711
|
-
if (merged._feedback === undefined)
|
|
9712
|
-
delete merged._feedback;
|
|
9713
|
-
if (merged._phaseState === undefined)
|
|
9714
|
-
delete merged._phaseState;
|
|
9715
|
-
// Validate
|
|
9716
|
-
const result = metaJsonSchema.safeParse(merged);
|
|
9717
|
-
if (!result.success) {
|
|
9718
|
-
throw new Error(`Meta validation failed: ${result.error.message}`);
|
|
9078
|
+
/** Error indicating a spawn was deliberately aborted. */
|
|
9079
|
+
class SpawnAbortedError extends Error {
|
|
9080
|
+
constructor(message = 'Synthesis was aborted') {
|
|
9081
|
+
super(message);
|
|
9082
|
+
this.name = 'SpawnAbortedError';
|
|
9719
9083
|
}
|
|
9720
|
-
// Write to specified path (lock staging) or default meta.json
|
|
9721
|
-
const filePath = options.outputPath ?? join(options.metaPath, 'meta.json');
|
|
9722
|
-
await writeFile(filePath, JSON.stringify(result.data, null, 2) + '\n');
|
|
9723
|
-
return result.data;
|
|
9724
9084
|
}
|
|
9725
9085
|
|
|
9726
9086
|
/**
|
|
9727
|
-
*
|
|
9087
|
+
* Error thrown when a spawned subprocess times out.
|
|
9728
9088
|
*
|
|
9729
|
-
*
|
|
9730
|
-
* the full discovery + ownership tree build. Discovers only immediate child
|
|
9731
|
-
* `.meta/` directories.
|
|
9089
|
+
* Carries the output file path so callers can attempt partial output recovery.
|
|
9732
9090
|
*
|
|
9733
|
-
* @module
|
|
9091
|
+
* @module executor/SpawnTimeoutError
|
|
9734
9092
|
*/
|
|
9093
|
+
/** Error indicating a spawn timeout with a recoverable output path. */
|
|
9094
|
+
class SpawnTimeoutError extends Error {
|
|
9095
|
+
/** Path to the (possibly partial) output file written before timeout. */
|
|
9096
|
+
outputPath;
|
|
9097
|
+
constructor(message, outputPath) {
|
|
9098
|
+
super(message);
|
|
9099
|
+
this.name = 'SpawnTimeoutError';
|
|
9100
|
+
this.outputPath = outputPath;
|
|
9101
|
+
}
|
|
9102
|
+
}
|
|
9103
|
+
|
|
9735
9104
|
/**
|
|
9736
|
-
*
|
|
9105
|
+
* MetaExecutor implementation using the OpenClaw gateway HTTP API.
|
|
9737
9106
|
*
|
|
9738
|
-
*
|
|
9739
|
-
*
|
|
9107
|
+
* Lives in the library package so both plugin and runner can import it.
|
|
9108
|
+
* Spawns sub-agent sessions via the gateway's `/tools/invoke` endpoint,
|
|
9109
|
+
* polls for completion, and extracts output text.
|
|
9740
9110
|
*
|
|
9741
|
-
* @
|
|
9742
|
-
* @param watcher - WatcherClient for filesystem enumeration.
|
|
9743
|
-
* @returns MetaNode with direct children wired.
|
|
9111
|
+
* @module executor/GatewayExecutor
|
|
9744
9112
|
*/
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9113
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
9114
|
+
const DEFAULT_TIMEOUT_MS$1 = 600_000; // 10 minutes
|
|
9115
|
+
/**
|
|
9116
|
+
* MetaExecutor that spawns OpenClaw sessions via the gateway's
|
|
9117
|
+
* `/tools/invoke` endpoint.
|
|
9118
|
+
*
|
|
9119
|
+
* Used by both the OpenClaw plugin (in-process tool calls) and the
|
|
9120
|
+
* runner/CLI (external invocation). Constructs from `gatewayUrl` and
|
|
9121
|
+
* optional `apiKey` — typically sourced from `MetaConfig`.
|
|
9122
|
+
*/
|
|
9123
|
+
class GatewayExecutor {
|
|
9124
|
+
gatewayUrl;
|
|
9125
|
+
apiKey;
|
|
9126
|
+
pollIntervalMs;
|
|
9127
|
+
workspaceDir;
|
|
9128
|
+
controller = new AbortController();
|
|
9129
|
+
constructor(options = {}) {
|
|
9130
|
+
this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:18789').replace(/\/+$/, '');
|
|
9131
|
+
this.apiKey = options.apiKey;
|
|
9132
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
9133
|
+
this.workspaceDir = options.workspaceDir ?? join(tmpdir(), 'jeeves-meta');
|
|
9766
9134
|
}
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9135
|
+
/** Remove a temp output file if it exists. */
|
|
9136
|
+
cleanupOutputFile(outputPath) {
|
|
9137
|
+
try {
|
|
9138
|
+
if (existsSync(outputPath))
|
|
9139
|
+
unlinkSync(outputPath);
|
|
9140
|
+
}
|
|
9141
|
+
catch {
|
|
9142
|
+
/* best-effort cleanup */
|
|
9143
|
+
}
|
|
9144
|
+
}
|
|
9145
|
+
/** Invoke a gateway tool via the /tools/invoke HTTP endpoint. */
|
|
9146
|
+
async invoke(tool, args, sessionKey) {
|
|
9147
|
+
const headers = {
|
|
9148
|
+
'Content-Type': 'application/json',
|
|
9149
|
+
};
|
|
9150
|
+
if (this.apiKey) {
|
|
9151
|
+
headers['Authorization'] = 'Bearer ' + this.apiKey;
|
|
9152
|
+
}
|
|
9153
|
+
const body = { tool, args };
|
|
9154
|
+
if (sessionKey)
|
|
9155
|
+
body.sessionKey = sessionKey;
|
|
9156
|
+
const res = await fetch(this.gatewayUrl + '/tools/invoke', {
|
|
9157
|
+
method: 'POST',
|
|
9158
|
+
headers,
|
|
9159
|
+
body: JSON.stringify(body),
|
|
9160
|
+
});
|
|
9161
|
+
if (!res.ok) {
|
|
9162
|
+
const text = await res.text();
|
|
9163
|
+
throw new Error(`Gateway ${tool} failed: HTTP ${res.status.toString()} - ${text}`);
|
|
9164
|
+
}
|
|
9165
|
+
const data = (await res.json());
|
|
9166
|
+
if (data.ok === false || data.error) {
|
|
9167
|
+
throw new Error(`Gateway ${tool} error: ${data.error?.message ?? JSON.stringify(data)}`);
|
|
9168
|
+
}
|
|
9169
|
+
return data;
|
|
9170
|
+
}
|
|
9171
|
+
/** Look up session metadata (tokens, completion status) via sessions_list. */
|
|
9172
|
+
async getSessionInfo(sessionKey) {
|
|
9173
|
+
try {
|
|
9174
|
+
const result = await this.invoke('sessions_list', {
|
|
9175
|
+
limit: 200,
|
|
9176
|
+
messageLimit: 0,
|
|
9177
|
+
});
|
|
9178
|
+
const sessions = (result.result?.details?.sessions ??
|
|
9179
|
+
result.result?.sessions ??
|
|
9180
|
+
[]);
|
|
9181
|
+
const match = sessions.find((s) => s.key === sessionKey);
|
|
9182
|
+
if (!match) {
|
|
9183
|
+
// Session absent from list — likely cleaned up after completion.
|
|
9184
|
+
// With limit=200 this is reliable; a false positive here only
|
|
9185
|
+
// means we read the output file slightly early (still correct
|
|
9186
|
+
// if the file exists).
|
|
9187
|
+
return { completed: true };
|
|
9188
|
+
}
|
|
9189
|
+
const done = match.status === 'completed' || match.status === 'done';
|
|
9190
|
+
return { tokens: match.totalTokens, completed: done };
|
|
9191
|
+
}
|
|
9192
|
+
catch {
|
|
9193
|
+
return { completed: false };
|
|
9194
|
+
}
|
|
9195
|
+
}
|
|
9196
|
+
/** Whether this executor has been aborted by the operator. */
|
|
9197
|
+
get aborted() {
|
|
9198
|
+
return this.controller.signal.aborted;
|
|
9199
|
+
}
|
|
9200
|
+
/** Abort the currently running spawn, if any. */
|
|
9201
|
+
abort() {
|
|
9202
|
+
this.controller.abort();
|
|
9203
|
+
}
|
|
9204
|
+
async spawn(task, options) {
|
|
9205
|
+
// Fresh controller for each spawn call
|
|
9206
|
+
this.controller = new AbortController();
|
|
9207
|
+
const timeoutSeconds = options?.timeout ?? DEFAULT_TIMEOUT_MS$1 / 1000;
|
|
9208
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
9209
|
+
const deadline = Date.now() + timeoutMs;
|
|
9210
|
+
// Ensure workspace dir exists
|
|
9211
|
+
if (!existsSync(this.workspaceDir)) {
|
|
9212
|
+
mkdirSync(this.workspaceDir, { recursive: true });
|
|
9213
|
+
}
|
|
9214
|
+
// Generate unique output path for file-based output
|
|
9215
|
+
const outputId = randomUUID();
|
|
9216
|
+
const outputPath = this.workspaceDir + '/output-' + outputId + '.json';
|
|
9217
|
+
// Append file output instruction to the task
|
|
9218
|
+
const taskWithOutput = task +
|
|
9219
|
+
'\n\n## OUTPUT DELIVERY\n\n' +
|
|
9220
|
+
'Write your complete output to a file using the Write tool at:\n' +
|
|
9221
|
+
outputPath +
|
|
9222
|
+
'\n\n' +
|
|
9223
|
+
'After writing the file, reply with ONLY: NO_REPLY';
|
|
9224
|
+
// Step 1: Spawn the sub-agent session (unique label per cycle to avoid
|
|
9225
|
+
// "label already in use" errors — gateway labels persist after session completion)
|
|
9226
|
+
const labelBase = options?.label ?? 'jeeves-meta-synthesis';
|
|
9227
|
+
const label = labelBase + '-' + outputId.slice(0, 8);
|
|
9228
|
+
const spawnResult = await this.invoke('sessions_spawn', {
|
|
9229
|
+
task: taskWithOutput,
|
|
9230
|
+
label,
|
|
9231
|
+
runTimeoutSeconds: timeoutSeconds,
|
|
9232
|
+
...(options?.thinking ? { thinking: options.thinking } : {}),
|
|
9233
|
+
...(options?.model ? { model: options.model } : {}),
|
|
9234
|
+
});
|
|
9235
|
+
const details = (spawnResult.result?.details ?? spawnResult.result);
|
|
9236
|
+
const sessionKey = details?.childSessionKey ?? details?.sessionKey;
|
|
9237
|
+
if (typeof sessionKey !== 'string' || !sessionKey) {
|
|
9238
|
+
throw new Error('Gateway sessions_spawn returned no sessionKey: ' +
|
|
9239
|
+
JSON.stringify(spawnResult));
|
|
9240
|
+
}
|
|
9241
|
+
// Step 2: Poll for completion via sessions_history
|
|
9242
|
+
await sleepAsync(3000);
|
|
9243
|
+
while (Date.now() < deadline) {
|
|
9244
|
+
// Check for abort before each poll iteration
|
|
9245
|
+
if (this.controller.signal.aborted) {
|
|
9246
|
+
this.cleanupOutputFile(outputPath);
|
|
9247
|
+
throw new SpawnAbortedError();
|
|
9248
|
+
}
|
|
9249
|
+
try {
|
|
9250
|
+
const historyResult = await this.invoke('sessions_history', {
|
|
9251
|
+
sessionKey,
|
|
9252
|
+
limit: 5,
|
|
9253
|
+
includeTools: false,
|
|
9254
|
+
});
|
|
9255
|
+
const messages = historyResult.result?.details?.messages ??
|
|
9256
|
+
historyResult.result?.messages ??
|
|
9257
|
+
[];
|
|
9258
|
+
const msgArray = messages;
|
|
9259
|
+
// Check 1: terminal stop reason in history
|
|
9260
|
+
let historyDone = false;
|
|
9261
|
+
if (msgArray.length > 0) {
|
|
9262
|
+
const lastMsg = msgArray[msgArray.length - 1];
|
|
9263
|
+
if (lastMsg.role === 'assistant' &&
|
|
9264
|
+
lastMsg.stopReason &&
|
|
9265
|
+
lastMsg.stopReason !== 'toolUse' &&
|
|
9266
|
+
lastMsg.stopReason !== 'error') {
|
|
9267
|
+
historyDone = true;
|
|
9268
|
+
}
|
|
9269
|
+
}
|
|
9270
|
+
// Check 2: session completion status via sessions_list
|
|
9271
|
+
const sessionInfo = await this.getSessionInfo(sessionKey);
|
|
9272
|
+
if (historyDone || sessionInfo.completed) {
|
|
9273
|
+
const tokens = sessionInfo.tokens;
|
|
9274
|
+
// Read output from file (sub-agent wrote it via Write tool)
|
|
9275
|
+
if (existsSync(outputPath)) {
|
|
9276
|
+
try {
|
|
9277
|
+
const output = readFileSync(outputPath, 'utf8');
|
|
9278
|
+
return { output, tokens };
|
|
9279
|
+
}
|
|
9280
|
+
finally {
|
|
9281
|
+
try {
|
|
9282
|
+
unlinkSync(outputPath);
|
|
9283
|
+
}
|
|
9284
|
+
catch {
|
|
9285
|
+
/* cleanup best-effort */
|
|
9286
|
+
}
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
9289
|
+
// Fallback: extract from message content if file wasn't written
|
|
9290
|
+
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9291
|
+
const msg = msgArray[i];
|
|
9292
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
9293
|
+
const text = typeof msg.content === 'string'
|
|
9294
|
+
? msg.content
|
|
9295
|
+
: Array.isArray(msg.content)
|
|
9296
|
+
? msg.content
|
|
9297
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
9298
|
+
.map((b) => b.text)
|
|
9299
|
+
.join('\n')
|
|
9300
|
+
: '';
|
|
9301
|
+
if (text)
|
|
9302
|
+
return { output: text, tokens };
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9305
|
+
return { output: '', tokens };
|
|
9306
|
+
}
|
|
9307
|
+
}
|
|
9308
|
+
catch {
|
|
9309
|
+
// Transient poll failure — keep trying
|
|
9310
|
+
}
|
|
9311
|
+
await sleepAsync(this.pollIntervalMs);
|
|
9312
|
+
}
|
|
9313
|
+
throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
|
|
9783
9314
|
}
|
|
9784
|
-
return node;
|
|
9785
9315
|
}
|
|
9786
9316
|
|
|
9787
9317
|
/**
|
|
9788
|
-
*
|
|
9318
|
+
* Pino logger factory.
|
|
9789
9319
|
*
|
|
9790
|
-
*
|
|
9320
|
+
* @module logger
|
|
9321
|
+
*/
|
|
9322
|
+
/**
|
|
9323
|
+
* Create a pino logger instance.
|
|
9791
9324
|
*
|
|
9792
|
-
* @
|
|
9325
|
+
* @param config - Optional logger configuration.
|
|
9326
|
+
* @returns Configured pino logger.
|
|
9793
9327
|
*/
|
|
9328
|
+
function createLogger(config) {
|
|
9329
|
+
const level = config?.level ?? 'info';
|
|
9330
|
+
if (config?.file) {
|
|
9331
|
+
const transport = pino.transport({
|
|
9332
|
+
target: 'pino/file',
|
|
9333
|
+
options: { destination: config.file, mkdir: true },
|
|
9334
|
+
});
|
|
9335
|
+
return pino({ level }, transport);
|
|
9336
|
+
}
|
|
9337
|
+
return pino({ level });
|
|
9338
|
+
}
|
|
9339
|
+
|
|
9794
9340
|
/**
|
|
9795
|
-
*
|
|
9341
|
+
* Built-in default prompts for the synthesis pipeline.
|
|
9796
9342
|
*
|
|
9797
|
-
*
|
|
9798
|
-
*
|
|
9343
|
+
* Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
|
|
9344
|
+
* Loaded at runtime relative to the compiled module location.
|
|
9799
9345
|
*
|
|
9800
|
-
*
|
|
9801
|
-
*
|
|
9346
|
+
* Users can override via `defaultArchitect` / `defaultCritic` in the service
|
|
9347
|
+
* config. Most installations should use the built-in defaults.
|
|
9802
9348
|
*
|
|
9803
|
-
* @
|
|
9804
|
-
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
9805
|
-
* @returns Same array with effectiveStaleness computed.
|
|
9349
|
+
* @module prompts
|
|
9806
9350
|
*/
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
return candidates.map((c, i) => {
|
|
9816
|
-
const emphasis = c.meta._emphasis ?? 1;
|
|
9817
|
-
return {
|
|
9818
|
-
...c,
|
|
9819
|
-
effectiveStaleness: c.actualStaleness *
|
|
9820
|
-
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
9821
|
-
};
|
|
9822
|
-
});
|
|
9823
|
-
}
|
|
9351
|
+
const packageRoot = packageDirectorySync({
|
|
9352
|
+
cwd: fileURLToPath(import.meta.url),
|
|
9353
|
+
});
|
|
9354
|
+
const promptDir = join(packageRoot, 'dist', 'prompts');
|
|
9355
|
+
/** Built-in default architect prompt. */
|
|
9356
|
+
const DEFAULT_ARCHITECT_PROMPT = readFileSync(join(promptDir, 'architect.md'), 'utf8');
|
|
9357
|
+
/** Built-in default critic prompt. */
|
|
9358
|
+
const DEFAULT_CRITIC_PROMPT = readFileSync(join(promptDir, 'critic.md'), 'utf8');
|
|
9824
9359
|
|
|
9825
9360
|
/**
|
|
9826
|
-
*
|
|
9361
|
+
* Build the MetaContext for a synthesis cycle.
|
|
9827
9362
|
*
|
|
9828
|
-
*
|
|
9363
|
+
* Computes shared inputs once: scope files, delta files, child meta outputs,
|
|
9364
|
+
* previous content/feedback, steer, and archive paths.
|
|
9829
9365
|
*
|
|
9830
|
-
* @module
|
|
9366
|
+
* @module orchestrator/contextPackage
|
|
9831
9367
|
*/
|
|
9832
9368
|
/**
|
|
9833
|
-
*
|
|
9369
|
+
* Condense a file list into glob-like summaries.
|
|
9370
|
+
* Groups by directory + extension pattern.
|
|
9834
9371
|
*
|
|
9835
|
-
* @param
|
|
9836
|
-
* @
|
|
9372
|
+
* @param files - Array of file paths.
|
|
9373
|
+
* @param maxIndividual - Show individual files up to this count.
|
|
9374
|
+
* @returns Condensed summary string.
|
|
9837
9375
|
*/
|
|
9838
|
-
function
|
|
9839
|
-
if (
|
|
9840
|
-
return
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
|
|
9376
|
+
function condenseScopeFiles(files, maxIndividual = 30) {
|
|
9377
|
+
if (files.length <= maxIndividual)
|
|
9378
|
+
return files.join('\n');
|
|
9379
|
+
// Group by dir + extension
|
|
9380
|
+
const groups = new Map();
|
|
9381
|
+
for (const f of files) {
|
|
9382
|
+
const dir = f.substring(0, f.lastIndexOf('/') + 1) || './';
|
|
9383
|
+
const ext = f.includes('.') ? f.substring(f.lastIndexOf('.')) : '(no ext)';
|
|
9384
|
+
const key = dir + '*' + ext;
|
|
9385
|
+
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
9846
9386
|
}
|
|
9847
|
-
|
|
9387
|
+
// Sort by count descending
|
|
9388
|
+
const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
9389
|
+
return sorted
|
|
9390
|
+
.map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
|
|
9391
|
+
.join('\n');
|
|
9848
9392
|
}
|
|
9849
9393
|
/**
|
|
9850
|
-
*
|
|
9851
|
-
*
|
|
9852
|
-
* Consolidates the repeated pattern of:
|
|
9853
|
-
* filter → computeEffectiveStaleness → selectCandidate → return path
|
|
9394
|
+
* Read a meta.json file and extract its `_content` field.
|
|
9854
9395
|
*
|
|
9855
|
-
* @param
|
|
9856
|
-
* @
|
|
9857
|
-
* @returns The stalest candidate's metaPath, or null if none are stale.
|
|
9396
|
+
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
9397
|
+
* @returns The `_content` string, or null if missing/unreadable.
|
|
9858
9398
|
*/
|
|
9859
|
-
function
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9399
|
+
async function readMetaContent(metaJsonPath) {
|
|
9400
|
+
try {
|
|
9401
|
+
const raw = await readFile(metaJsonPath, 'utf8');
|
|
9402
|
+
const meta = JSON.parse(raw);
|
|
9403
|
+
return meta._content ?? null;
|
|
9404
|
+
}
|
|
9405
|
+
catch {
|
|
9406
|
+
return null;
|
|
9407
|
+
}
|
|
9863
9408
|
}
|
|
9864
|
-
|
|
9865
|
-
/**
|
|
9866
|
-
* Shared error utilities.
|
|
9867
|
-
*
|
|
9868
|
-
* @module errors
|
|
9869
|
-
*/
|
|
9870
9409
|
/**
|
|
9871
|
-
*
|
|
9410
|
+
* Build the context package for a synthesis cycle.
|
|
9872
9411
|
*
|
|
9873
|
-
* @param
|
|
9874
|
-
* @param
|
|
9875
|
-
* @param
|
|
9876
|
-
* @returns
|
|
9412
|
+
* @param node - The meta node being synthesized.
|
|
9413
|
+
* @param meta - Current meta.json content.
|
|
9414
|
+
* @param watcher - WatcherClient for scope enumeration.
|
|
9415
|
+
* @returns The computed context package.
|
|
9877
9416
|
*/
|
|
9878
|
-
function
|
|
9417
|
+
async function buildContextPackage(node, meta, watcher, logger) {
|
|
9418
|
+
// Scope and delta files via watcher walk
|
|
9419
|
+
const scopeStart = Date.now();
|
|
9420
|
+
const { scopeFiles } = await getScopeFiles(node, watcher, logger);
|
|
9421
|
+
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
9422
|
+
logger?.debug({
|
|
9423
|
+
scopeFiles: scopeFiles.length,
|
|
9424
|
+
deltaFiles: deltaFiles.length,
|
|
9425
|
+
durationMs: Date.now() - scopeStart,
|
|
9426
|
+
}, 'scope and delta files computed');
|
|
9427
|
+
// Child meta outputs (parallel reads)
|
|
9428
|
+
const childMetas = {};
|
|
9429
|
+
const childEntries = await Promise.all(node.children.map(async (child) => {
|
|
9430
|
+
const content = await readMetaContent(join(child.metaPath, 'meta.json'));
|
|
9431
|
+
return [child.ownerPath, content];
|
|
9432
|
+
}));
|
|
9433
|
+
for (const [path, content] of childEntries) {
|
|
9434
|
+
childMetas[path] = content;
|
|
9435
|
+
}
|
|
9436
|
+
// Cross-referenced meta outputs (parallel reads)
|
|
9437
|
+
const crossRefMetas = {};
|
|
9438
|
+
const seen = new Set();
|
|
9439
|
+
const crossRefPaths = [];
|
|
9440
|
+
for (const refPath of meta._crossRefs ?? []) {
|
|
9441
|
+
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
9442
|
+
continue;
|
|
9443
|
+
if (seen.has(refPath))
|
|
9444
|
+
continue;
|
|
9445
|
+
seen.add(refPath);
|
|
9446
|
+
crossRefPaths.push(refPath);
|
|
9447
|
+
}
|
|
9448
|
+
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
9449
|
+
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
9450
|
+
return [refPath, content];
|
|
9451
|
+
}));
|
|
9452
|
+
for (const [path, content] of crossRefEntries) {
|
|
9453
|
+
crossRefMetas[path] = content;
|
|
9454
|
+
}
|
|
9455
|
+
// Archive paths
|
|
9456
|
+
const archives = listArchiveFiles(node.metaPath);
|
|
9879
9457
|
return {
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9458
|
+
path: node.metaPath,
|
|
9459
|
+
scopeFiles,
|
|
9460
|
+
deltaFiles,
|
|
9461
|
+
childMetas,
|
|
9462
|
+
crossRefMetas,
|
|
9463
|
+
previousContent: meta._content ?? null,
|
|
9464
|
+
previousFeedback: meta._feedback ?? null,
|
|
9465
|
+
steer: meta._steer ?? null,
|
|
9466
|
+
previousState: meta._state ?? null,
|
|
9467
|
+
archives,
|
|
9883
9468
|
};
|
|
9884
9469
|
}
|
|
9885
9470
|
|
|
9886
9471
|
/**
|
|
9887
|
-
*
|
|
9888
|
-
*
|
|
9889
|
-
* Used to detect when directory structure changes, triggering
|
|
9890
|
-
* an architect re-run.
|
|
9891
|
-
*
|
|
9892
|
-
* @module structureHash
|
|
9893
|
-
*/
|
|
9894
|
-
/**
|
|
9895
|
-
* Compute a SHA-256 hash of a sorted file listing.
|
|
9896
|
-
*
|
|
9897
|
-
* @param filePaths - Array of file paths in scope.
|
|
9898
|
-
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
9899
|
-
*/
|
|
9900
|
-
function computeStructureHash(filePaths) {
|
|
9901
|
-
const sorted = [...filePaths].sort();
|
|
9902
|
-
const content = sorted.join('\n');
|
|
9903
|
-
return createHash('sha256').update(content).digest('hex');
|
|
9904
|
-
}
|
|
9905
|
-
|
|
9906
|
-
/**
|
|
9907
|
-
* Lock-staged cycle finalization: write to .lock, copy to meta.json, archive, prune.
|
|
9908
|
-
*
|
|
9909
|
-
* @module orchestrator/finalizeCycle
|
|
9910
|
-
*/
|
|
9911
|
-
/** Finalize a cycle using lock staging: write to .lock → copy to meta.json + archive → delete .lock. */
|
|
9912
|
-
async function finalizeCycle(opts) {
|
|
9913
|
-
const lockPath = join(opts.metaPath, '.lock');
|
|
9914
|
-
const metaJsonPath = join(opts.metaPath, 'meta.json');
|
|
9915
|
-
// Stage: write merged result to .lock (sequential — ordering matters)
|
|
9916
|
-
const updated = await mergeAndWrite({
|
|
9917
|
-
metaPath: opts.metaPath,
|
|
9918
|
-
current: opts.current,
|
|
9919
|
-
architect: opts.architect,
|
|
9920
|
-
builder: opts.builder,
|
|
9921
|
-
critic: opts.critic,
|
|
9922
|
-
builderOutput: opts.builderOutput,
|
|
9923
|
-
feedback: opts.feedback,
|
|
9924
|
-
structureHash: opts.structureHash,
|
|
9925
|
-
synthesisCount: opts.synthesisCount,
|
|
9926
|
-
error: opts.error,
|
|
9927
|
-
architectTokens: opts.architectTokens,
|
|
9928
|
-
builderTokens: opts.builderTokens,
|
|
9929
|
-
criticTokens: opts.criticTokens,
|
|
9930
|
-
outputPath: lockPath,
|
|
9931
|
-
state: opts.state,
|
|
9932
|
-
stateOnly: opts.stateOnly,
|
|
9933
|
-
});
|
|
9934
|
-
// Commit: copy .lock → meta.json
|
|
9935
|
-
await copyFile(lockPath, metaJsonPath);
|
|
9936
|
-
// Archive + prune from the committed meta.json (sequential)
|
|
9937
|
-
await createSnapshot(opts.metaPath, updated);
|
|
9938
|
-
await pruneArchive(opts.metaPath, opts.config.maxArchive);
|
|
9939
|
-
// .lock is cleaned up by the finally block (releaseLock)
|
|
9940
|
-
return updated;
|
|
9941
|
-
}
|
|
9942
|
-
|
|
9943
|
-
/**
|
|
9944
|
-
* Parse subprocess outputs for each synthesis step.
|
|
9945
|
-
*
|
|
9946
|
-
* - Architect: returns text \> _builder
|
|
9947
|
-
* - Builder: returns JSON \> _content + structured fields
|
|
9948
|
-
* - Critic: returns text \> _feedback
|
|
9472
|
+
* Build task prompts for each synthesis step.
|
|
9949
9473
|
*
|
|
9950
|
-
*
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
* Parse architect output. The architect returns a task brief as text.
|
|
9474
|
+
* Prompts are compiled as Handlebars templates with access to config,
|
|
9475
|
+
* meta, and scope context. The architect can write template expressions
|
|
9476
|
+
* into its _builder output; these resolve when the builder task is compiled.
|
|
9954
9477
|
*
|
|
9955
|
-
* @
|
|
9956
|
-
* @returns The task brief string.
|
|
9478
|
+
* @module orchestrator/buildTask
|
|
9957
9479
|
*/
|
|
9958
|
-
|
|
9959
|
-
|
|
9480
|
+
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
9481
|
+
/** Build the template context from synthesis inputs. */
|
|
9482
|
+
function buildTemplateContext(ctx, meta, config) {
|
|
9483
|
+
return {
|
|
9484
|
+
config,
|
|
9485
|
+
meta,
|
|
9486
|
+
scope: {
|
|
9487
|
+
fileCount: ctx.scopeFiles.length,
|
|
9488
|
+
deltaCount: ctx.deltaFiles.length,
|
|
9489
|
+
childCount: Object.keys(ctx.childMetas).length,
|
|
9490
|
+
crossRefCount: Object.keys(ctx.crossRefMetas).length,
|
|
9491
|
+
},
|
|
9492
|
+
};
|
|
9960
9493
|
}
|
|
9961
9494
|
/**
|
|
9962
|
-
*
|
|
9963
|
-
*
|
|
9964
|
-
* Attempts JSON parse first. If that fails, treats the entire output as _content.
|
|
9965
|
-
*
|
|
9966
|
-
* @param output - Raw subprocess output.
|
|
9967
|
-
* @returns Parsed builder output with content and structured fields.
|
|
9495
|
+
* Compile a string as a Handlebars template with the given context.
|
|
9496
|
+
* Returns the original string unchanged if compilation fails.
|
|
9968
9497
|
*/
|
|
9969
|
-
function
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
const direct = tryParseJson(trimmed);
|
|
9973
|
-
if (direct)
|
|
9974
|
-
return direct;
|
|
9975
|
-
// Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
|
|
9976
|
-
const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
9977
|
-
const fenceMatches = [];
|
|
9978
|
-
let match;
|
|
9979
|
-
while ((match = fencePattern.exec(trimmed)) !== null) {
|
|
9980
|
-
fenceMatches.push(match[1].trim());
|
|
9498
|
+
function compileTemplate(text, context) {
|
|
9499
|
+
try {
|
|
9500
|
+
return Handlebars.compile(text, { noEscape: true })(context);
|
|
9981
9501
|
}
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
const result = tryParseJson(fenceMatches[i]);
|
|
9985
|
-
if (result)
|
|
9986
|
-
return result;
|
|
9502
|
+
catch {
|
|
9503
|
+
return text;
|
|
9987
9504
|
}
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
if (
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9505
|
+
}
|
|
9506
|
+
/** Append a keyed record of meta outputs as subsections, if non-empty. */
|
|
9507
|
+
function appendMetaSections(sections, heading, metas) {
|
|
9508
|
+
if (Object.keys(metas).length === 0)
|
|
9509
|
+
return;
|
|
9510
|
+
sections.push('', heading);
|
|
9511
|
+
for (const [path, content] of Object.entries(metas)) {
|
|
9512
|
+
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
9995
9513
|
}
|
|
9996
|
-
// Fallback: treat entire output as content
|
|
9997
|
-
return { content: trimmed, fields: {} };
|
|
9998
9514
|
}
|
|
9999
|
-
/**
|
|
10000
|
-
function
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
: null;
|
|
10013
|
-
if (content === null)
|
|
10014
|
-
return null;
|
|
10015
|
-
// Extract _state (the ONLY underscore key the builder is allowed to set)
|
|
10016
|
-
const state = '_state' in parsed ? parsed['_state'] : undefined;
|
|
10017
|
-
// Extract non-underscore fields
|
|
10018
|
-
const fields = {};
|
|
10019
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
10020
|
-
if (!key.startsWith('_') && key !== 'content') {
|
|
10021
|
-
fields[key] = value;
|
|
10022
|
-
}
|
|
10023
|
-
}
|
|
10024
|
-
return { content, fields, ...(state !== undefined ? { state } : {}) };
|
|
9515
|
+
/** Append optional context sections shared across all step prompts. */
|
|
9516
|
+
function appendSharedSections(sections, ctx, options) {
|
|
9517
|
+
const opts = {
|
|
9518
|
+
includeSteer: true,
|
|
9519
|
+
includePreviousContent: true,
|
|
9520
|
+
includePreviousFeedback: true,
|
|
9521
|
+
feedbackHeading: '## PREVIOUS FEEDBACK',
|
|
9522
|
+
includeChildMetas: true,
|
|
9523
|
+
includeCrossRefs: true,
|
|
9524
|
+
...options,
|
|
9525
|
+
};
|
|
9526
|
+
if (opts.includeSteer && ctx.steer) {
|
|
9527
|
+
sections.push('', '## STEERING PROMPT', ctx.steer);
|
|
10025
9528
|
}
|
|
10026
|
-
|
|
10027
|
-
|
|
9529
|
+
if (opts.includePreviousContent && ctx.previousContent) {
|
|
9530
|
+
sections.push('', '## PREVIOUS SYNTHESIS', ctx.previousContent);
|
|
9531
|
+
}
|
|
9532
|
+
if (opts.includePreviousFeedback && ctx.previousFeedback) {
|
|
9533
|
+
sections.push('', opts.feedbackHeading, ctx.previousFeedback);
|
|
9534
|
+
}
|
|
9535
|
+
if (opts.includeChildMetas) {
|
|
9536
|
+
appendMetaSections(sections, '## CHILD META OUTPUTS', ctx.childMetas);
|
|
9537
|
+
}
|
|
9538
|
+
if (opts.includeCrossRefs) {
|
|
9539
|
+
appendMetaSections(sections, '## CROSS-REFERENCED METAS', ctx.crossRefMetas);
|
|
10028
9540
|
}
|
|
10029
9541
|
}
|
|
10030
9542
|
/**
|
|
10031
|
-
*
|
|
9543
|
+
* Build the architect task prompt.
|
|
10032
9544
|
*
|
|
10033
|
-
* @param
|
|
10034
|
-
* @
|
|
9545
|
+
* @param ctx - Synthesis context.
|
|
9546
|
+
* @param meta - Current meta.json.
|
|
9547
|
+
* @param config - Synthesis config.
|
|
9548
|
+
* @returns The architect task prompt string.
|
|
10035
9549
|
*/
|
|
10036
|
-
function
|
|
10037
|
-
|
|
9550
|
+
function buildArchitectTask(ctx, meta, config) {
|
|
9551
|
+
const sections = [
|
|
9552
|
+
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
9553
|
+
'',
|
|
9554
|
+
meta._architect ?? config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
|
|
9555
|
+
'',
|
|
9556
|
+
'## SCOPE',
|
|
9557
|
+
`Path: ${ctx.path}`,
|
|
9558
|
+
`Total files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
9559
|
+
`Files changed since last synthesis: ${ctx.deltaFiles.length.toString()}`,
|
|
9560
|
+
'',
|
|
9561
|
+
'### File listing (scope)',
|
|
9562
|
+
condenseScopeFiles(ctx.scopeFiles),
|
|
9563
|
+
];
|
|
9564
|
+
// Inject previous _builder so architect can see its own prior output
|
|
9565
|
+
if (meta._builder) {
|
|
9566
|
+
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
9567
|
+
}
|
|
9568
|
+
appendSharedSections(sections, ctx);
|
|
9569
|
+
if (ctx.archives.length > 0) {
|
|
9570
|
+
sections.push('', '## ARCHIVE HISTORY', `${ctx.archives.length.toString()} previous synthesis snapshots available in .meta/archive/.`, 'Review these to understand how the synthesis has evolved over time.');
|
|
9571
|
+
}
|
|
9572
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
10038
9573
|
}
|
|
10039
|
-
|
|
10040
|
-
/**
|
|
10041
|
-
* Timeout recovery — salvage partial builder state after a SpawnTimeoutError.
|
|
10042
|
-
*
|
|
10043
|
-
* @module orchestrator/timeoutRecovery
|
|
10044
|
-
*/
|
|
10045
9574
|
/**
|
|
10046
|
-
*
|
|
9575
|
+
* Build the builder task prompt.
|
|
10047
9576
|
*
|
|
10048
|
-
*
|
|
10049
|
-
*
|
|
9577
|
+
* @param ctx - Synthesis context.
|
|
9578
|
+
* @param meta - Current meta.json.
|
|
9579
|
+
* @param config - Synthesis config.
|
|
9580
|
+
* @returns The builder task prompt string.
|
|
10050
9581
|
*/
|
|
10051
|
-
|
|
10052
|
-
const
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
const timeoutError = {
|
|
10066
|
-
step: 'builder',
|
|
10067
|
-
code: 'TIMEOUT',
|
|
10068
|
-
message: err.message,
|
|
10069
|
-
};
|
|
10070
|
-
await finalizeCycle({
|
|
10071
|
-
metaPath,
|
|
10072
|
-
current: currentMeta,
|
|
10073
|
-
config,
|
|
10074
|
-
architect: currentMeta._architect ?? '',
|
|
10075
|
-
builder: builderBrief,
|
|
10076
|
-
critic: currentMeta._critic ?? '',
|
|
10077
|
-
builderOutput: null,
|
|
10078
|
-
feedback: null,
|
|
10079
|
-
structureHash,
|
|
10080
|
-
synthesisCount,
|
|
10081
|
-
error: timeoutError,
|
|
10082
|
-
state: partialOutput.state,
|
|
10083
|
-
stateOnly: true,
|
|
10084
|
-
});
|
|
10085
|
-
return {
|
|
10086
|
-
synthesized: true,
|
|
10087
|
-
metaPath,
|
|
10088
|
-
error: timeoutError,
|
|
10089
|
-
};
|
|
10090
|
-
}
|
|
9582
|
+
function buildBuilderTask(ctx, meta, config) {
|
|
9583
|
+
const sections = [
|
|
9584
|
+
`# jeeves-meta · BUILDER · ${ctx.path}`,
|
|
9585
|
+
'',
|
|
9586
|
+
'## TASK BRIEF (from Architect)',
|
|
9587
|
+
meta._builder ?? '(No architect brief available)',
|
|
9588
|
+
'',
|
|
9589
|
+
'## SCOPE',
|
|
9590
|
+
`Path: ${ctx.path}`,
|
|
9591
|
+
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
9592
|
+
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
9593
|
+
];
|
|
9594
|
+
if (ctx.previousState != null) {
|
|
9595
|
+
sections.push('', '## PREVIOUS STATE', 'The following opaque state was returned by the previous synthesis cycle.', 'Use it to continue progressive work. Update `_state` in your output to', 'reflect your progress.', '', '```json', JSON.stringify(ctx.previousState, null, 2), '```');
|
|
10091
9596
|
}
|
|
10092
|
-
|
|
9597
|
+
appendSharedSections(sections, ctx, {
|
|
9598
|
+
includeSteer: false,
|
|
9599
|
+
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
9600
|
+
});
|
|
9601
|
+
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.');
|
|
9602
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
10093
9603
|
}
|
|
10094
|
-
|
|
10095
9604
|
/**
|
|
10096
|
-
*
|
|
9605
|
+
* Build the critic task prompt.
|
|
10097
9606
|
*
|
|
10098
|
-
* @
|
|
9607
|
+
* @param ctx - Synthesis context.
|
|
9608
|
+
* @param meta - Current meta.json (with _content already set by builder).
|
|
9609
|
+
* @param config - Synthesis config.
|
|
9610
|
+
* @returns The critic task prompt string.
|
|
10099
9611
|
*/
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
currentMeta._generatedAt = new Date().toISOString();
|
|
10119
|
-
const lockPath = join(node.metaPath, '.lock');
|
|
10120
|
-
const metaJsonPath = join(node.metaPath, 'meta.json');
|
|
10121
|
-
await writeFile(lockPath, JSON.stringify(currentMeta, null, 2));
|
|
10122
|
-
await copyFile(lockPath, metaJsonPath);
|
|
10123
|
-
logger?.debug({ path: node.ownerPath }, 'Skipping empty-scope entity');
|
|
10124
|
-
return { synthesized: false };
|
|
10125
|
-
}
|
|
10126
|
-
// Step 5 (deferred): Structure hash from context scope files
|
|
10127
|
-
const newStructureHash = computeStructureHash(ctx.scopeFiles);
|
|
10128
|
-
const structureChanged = newStructureHash !== currentMeta._structureHash;
|
|
10129
|
-
// Step 8: Architect (conditional)
|
|
10130
|
-
const architectTriggered = isArchitectTriggered(currentMeta, structureChanged, steerChanged, config.architectEvery);
|
|
10131
|
-
let builderBrief = currentMeta._builder ?? '';
|
|
10132
|
-
let synthesisCount = currentMeta._synthesisCount ?? 0;
|
|
10133
|
-
let stepError = null;
|
|
10134
|
-
let architectTokens;
|
|
10135
|
-
let builderTokens;
|
|
10136
|
-
let criticTokens;
|
|
10137
|
-
// Shared base options for all finalizeCycle calls.
|
|
10138
|
-
// Note: synthesisCount is excluded because it mutates during the pipeline.
|
|
10139
|
-
const baseFinalizeOptions = {
|
|
10140
|
-
metaPath: node.metaPath,
|
|
10141
|
-
current: currentMeta,
|
|
10142
|
-
config,
|
|
10143
|
-
architect: currentMeta._architect ?? '',
|
|
10144
|
-
critic: currentMeta._critic ?? '',
|
|
10145
|
-
structureHash: newStructureHash,
|
|
10146
|
-
};
|
|
10147
|
-
if (architectTriggered) {
|
|
10148
|
-
try {
|
|
10149
|
-
await onProgress?.({
|
|
10150
|
-
type: 'phase_start',
|
|
10151
|
-
path: node.ownerPath,
|
|
10152
|
-
phase: 'architect',
|
|
10153
|
-
});
|
|
10154
|
-
const phaseStart = Date.now();
|
|
10155
|
-
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
10156
|
-
const architectResult = await executor.spawn(architectTask, {
|
|
10157
|
-
thinking: config.thinking,
|
|
10158
|
-
timeout: config.architectTimeout,
|
|
10159
|
-
label: 'meta-architect',
|
|
10160
|
-
});
|
|
10161
|
-
builderBrief = parseArchitectOutput(architectResult.output);
|
|
10162
|
-
architectTokens = architectResult.tokens;
|
|
10163
|
-
synthesisCount = 0;
|
|
10164
|
-
await onProgress?.({
|
|
10165
|
-
type: 'phase_complete',
|
|
10166
|
-
path: node.ownerPath,
|
|
10167
|
-
phase: 'architect',
|
|
10168
|
-
tokens: architectTokens,
|
|
10169
|
-
durationMs: Date.now() - phaseStart,
|
|
10170
|
-
});
|
|
10171
|
-
}
|
|
10172
|
-
catch (err) {
|
|
10173
|
-
stepError = toMetaError('architect', err);
|
|
10174
|
-
if (!currentMeta._builder) {
|
|
10175
|
-
// No cached builder — cycle fails
|
|
10176
|
-
await finalizeCycle({
|
|
10177
|
-
...baseFinalizeOptions,
|
|
10178
|
-
builder: '',
|
|
10179
|
-
builderOutput: null,
|
|
10180
|
-
feedback: null,
|
|
10181
|
-
synthesisCount,
|
|
10182
|
-
error: stepError,
|
|
10183
|
-
architectTokens,
|
|
10184
|
-
});
|
|
10185
|
-
return {
|
|
10186
|
-
synthesized: true,
|
|
10187
|
-
metaPath: node.metaPath,
|
|
10188
|
-
error: stepError,
|
|
10189
|
-
};
|
|
10190
|
-
}
|
|
10191
|
-
// Has cached builder — continue with existing
|
|
10192
|
-
}
|
|
10193
|
-
}
|
|
10194
|
-
// Step 9: Builder
|
|
10195
|
-
const metaForBuilder = { ...currentMeta, _builder: builderBrief };
|
|
10196
|
-
let builderOutput;
|
|
10197
|
-
try {
|
|
10198
|
-
await onProgress?.({
|
|
10199
|
-
type: 'phase_start',
|
|
10200
|
-
path: node.ownerPath,
|
|
10201
|
-
phase: 'builder',
|
|
10202
|
-
});
|
|
10203
|
-
const builderStart = Date.now();
|
|
10204
|
-
const builderTask = buildBuilderTask(ctx, metaForBuilder, config);
|
|
10205
|
-
const builderResult = await executor.spawn(builderTask, {
|
|
10206
|
-
thinking: config.thinking,
|
|
10207
|
-
timeout: config.builderTimeout,
|
|
10208
|
-
label: 'meta-builder',
|
|
10209
|
-
});
|
|
10210
|
-
builderOutput = parseBuilderOutput(builderResult.output);
|
|
10211
|
-
builderTokens = builderResult.tokens;
|
|
10212
|
-
synthesisCount++;
|
|
10213
|
-
await onProgress?.({
|
|
10214
|
-
type: 'phase_complete',
|
|
10215
|
-
path: node.ownerPath,
|
|
10216
|
-
phase: 'builder',
|
|
10217
|
-
tokens: builderTokens,
|
|
10218
|
-
durationMs: Date.now() - builderStart,
|
|
10219
|
-
});
|
|
10220
|
-
}
|
|
10221
|
-
catch (err) {
|
|
10222
|
-
if (err instanceof SpawnTimeoutError) {
|
|
10223
|
-
const recovered = await attemptTimeoutRecovery({
|
|
10224
|
-
err,
|
|
10225
|
-
currentMeta,
|
|
10226
|
-
metaPath: node.metaPath,
|
|
10227
|
-
config,
|
|
10228
|
-
builderBrief,
|
|
10229
|
-
structureHash: newStructureHash,
|
|
10230
|
-
synthesisCount,
|
|
10231
|
-
});
|
|
10232
|
-
if (recovered)
|
|
10233
|
-
return recovered;
|
|
10234
|
-
}
|
|
10235
|
-
stepError = toMetaError('builder', err);
|
|
10236
|
-
await finalizeCycle({
|
|
10237
|
-
...baseFinalizeOptions,
|
|
10238
|
-
builder: builderBrief,
|
|
10239
|
-
builderOutput: null,
|
|
10240
|
-
feedback: null,
|
|
10241
|
-
synthesisCount,
|
|
10242
|
-
error: stepError,
|
|
10243
|
-
});
|
|
10244
|
-
return { synthesized: true, metaPath: node.metaPath, error: stepError };
|
|
10245
|
-
}
|
|
10246
|
-
// Step 10: Critic
|
|
10247
|
-
const metaForCritic = {
|
|
10248
|
-
...currentMeta,
|
|
10249
|
-
_content: builderOutput.content,
|
|
10250
|
-
};
|
|
10251
|
-
let feedback = null;
|
|
10252
|
-
try {
|
|
10253
|
-
await onProgress?.({
|
|
10254
|
-
type: 'phase_start',
|
|
10255
|
-
path: node.ownerPath,
|
|
10256
|
-
phase: 'critic',
|
|
10257
|
-
});
|
|
10258
|
-
const criticStart = Date.now();
|
|
10259
|
-
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
10260
|
-
const criticResult = await executor.spawn(criticTask, {
|
|
10261
|
-
thinking: config.thinking,
|
|
10262
|
-
timeout: config.criticTimeout,
|
|
10263
|
-
label: 'meta-critic',
|
|
10264
|
-
});
|
|
10265
|
-
feedback = parseCriticOutput(criticResult.output);
|
|
10266
|
-
criticTokens = criticResult.tokens;
|
|
10267
|
-
stepError = null; // Clear any architect error on full success
|
|
10268
|
-
await onProgress?.({
|
|
10269
|
-
type: 'phase_complete',
|
|
10270
|
-
path: node.ownerPath,
|
|
10271
|
-
phase: 'critic',
|
|
10272
|
-
tokens: criticTokens,
|
|
10273
|
-
durationMs: Date.now() - criticStart,
|
|
10274
|
-
});
|
|
10275
|
-
}
|
|
10276
|
-
catch (err) {
|
|
10277
|
-
stepError = stepError ?? toMetaError('critic', err);
|
|
10278
|
-
}
|
|
10279
|
-
// Steps 11-12: Merge, archive, prune
|
|
10280
|
-
await finalizeCycle({
|
|
10281
|
-
...baseFinalizeOptions,
|
|
10282
|
-
builder: builderBrief,
|
|
10283
|
-
builderOutput,
|
|
10284
|
-
feedback,
|
|
10285
|
-
synthesisCount,
|
|
10286
|
-
error: stepError,
|
|
10287
|
-
architectTokens,
|
|
10288
|
-
builderTokens,
|
|
10289
|
-
criticTokens,
|
|
10290
|
-
state: builderOutput.state,
|
|
9612
|
+
function buildCriticTask(ctx, meta, config) {
|
|
9613
|
+
const sections = [
|
|
9614
|
+
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
9615
|
+
'',
|
|
9616
|
+
meta._critic ?? config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
9617
|
+
'',
|
|
9618
|
+
'## SYNTHESIS TO EVALUATE',
|
|
9619
|
+
meta._content ?? '(No content produced)',
|
|
9620
|
+
'',
|
|
9621
|
+
'## SCOPE',
|
|
9622
|
+
`Path: ${ctx.path}`,
|
|
9623
|
+
`Files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
9624
|
+
];
|
|
9625
|
+
appendSharedSections(sections, ctx, {
|
|
9626
|
+
includePreviousContent: false,
|
|
9627
|
+
feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
|
|
9628
|
+
includeChildMetas: false,
|
|
9629
|
+
includeCrossRefs: false,
|
|
10291
9630
|
});
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
metaPath: node.metaPath,
|
|
10295
|
-
error: stepError ?? undefined,
|
|
10296
|
-
};
|
|
9631
|
+
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
9632
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
10297
9633
|
}
|
|
10298
9634
|
|
|
10299
9635
|
/**
|
|
10300
|
-
*
|
|
9636
|
+
* Build a minimal MetaNode from a known meta path using watcher walk.
|
|
9637
|
+
*
|
|
9638
|
+
* Used for targeted synthesis (when a specific path is requested) to avoid
|
|
9639
|
+
* the full discovery + ownership tree build. Discovers only immediate child
|
|
9640
|
+
* `.meta/` directories.
|
|
10301
9641
|
*
|
|
10302
|
-
* @module
|
|
9642
|
+
* @module discovery/buildMinimalNode
|
|
10303
9643
|
*/
|
|
10304
|
-
async function orchestrateOnce(config, executor, watcher, targetPath, onProgress, logger) {
|
|
10305
|
-
// When targetPath is provided, skip the expensive full discovery scan.
|
|
10306
|
-
// Build a minimal node from the filesystem instead.
|
|
10307
|
-
if (targetPath) {
|
|
10308
|
-
const normalizedTarget = normalizePath(targetPath);
|
|
10309
|
-
const targetMetaJson = join(normalizedTarget, 'meta.json');
|
|
10310
|
-
if (!existsSync(targetMetaJson))
|
|
10311
|
-
return { synthesized: false };
|
|
10312
|
-
const node = await buildMinimalNode(normalizedTarget, watcher);
|
|
10313
|
-
if (!acquireLock(node.metaPath))
|
|
10314
|
-
return { synthesized: false };
|
|
10315
|
-
try {
|
|
10316
|
-
const currentMeta = await readMetaJson(normalizedTarget);
|
|
10317
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
10318
|
-
}
|
|
10319
|
-
finally {
|
|
10320
|
-
releaseLock(node.metaPath);
|
|
10321
|
-
}
|
|
10322
|
-
}
|
|
10323
|
-
// Full discovery path (scheduler-driven, no specific target)
|
|
10324
|
-
// Step 1: Discover via watcher walk
|
|
10325
|
-
const discoveryStart = Date.now();
|
|
10326
|
-
const metaPaths = await discoverMetas(watcher);
|
|
10327
|
-
logger?.debug({ paths: metaPaths.length, durationMs: Date.now() - discoveryStart }, 'discovery complete');
|
|
10328
|
-
if (metaPaths.length === 0)
|
|
10329
|
-
return { synthesized: false };
|
|
10330
|
-
// Read meta.json for each discovered meta
|
|
10331
|
-
const metas = new Map();
|
|
10332
|
-
for (const mp of metaPaths) {
|
|
10333
|
-
try {
|
|
10334
|
-
metas.set(normalizePath(mp), await readMetaJson(mp));
|
|
10335
|
-
}
|
|
10336
|
-
catch {
|
|
10337
|
-
// Skip metas with unreadable meta.json
|
|
10338
|
-
continue;
|
|
10339
|
-
}
|
|
10340
|
-
}
|
|
10341
|
-
// Only build tree from paths with readable meta.json (excludes orphaned/deleted entries)
|
|
10342
|
-
const validPaths = metaPaths.filter((mp) => metas.has(normalizePath(mp)));
|
|
10343
|
-
if (validPaths.length === 0)
|
|
10344
|
-
return { synthesized: false };
|
|
10345
|
-
const tree = buildOwnershipTree(validPaths);
|
|
10346
|
-
// Steps 3-4: Staleness check + candidate selection
|
|
10347
|
-
const candidates = [];
|
|
10348
|
-
for (const treeNode of tree.nodes.values()) {
|
|
10349
|
-
const meta = metas.get(treeNode.metaPath);
|
|
10350
|
-
if (!meta)
|
|
10351
|
-
continue;
|
|
10352
|
-
const staleness = actualStaleness(meta);
|
|
10353
|
-
if (staleness > 0) {
|
|
10354
|
-
candidates.push({ node: treeNode, meta, actualStaleness: staleness });
|
|
10355
|
-
}
|
|
10356
|
-
}
|
|
10357
|
-
const weighted = computeEffectiveStaleness(candidates, config.depthWeight);
|
|
10358
|
-
// Sort by effective staleness descending
|
|
10359
|
-
const ranked = [...weighted].sort((a, b) => b.effectiveStaleness - a.effectiveStaleness);
|
|
10360
|
-
if (ranked.length === 0)
|
|
10361
|
-
return { synthesized: false };
|
|
10362
|
-
// Find the first candidate with actual changes (if skipUnchanged)
|
|
10363
|
-
let winner = null;
|
|
10364
|
-
for (const candidate of ranked) {
|
|
10365
|
-
if (!acquireLock(candidate.node.metaPath))
|
|
10366
|
-
continue;
|
|
10367
|
-
const verifiedStale = await isStale(getScopePrefix(candidate.node), candidate.meta, watcher);
|
|
10368
|
-
if (!verifiedStale && candidate.meta._generatedAt) {
|
|
10369
|
-
// Bump _generatedAt so it doesn't win next cycle
|
|
10370
|
-
const freshMeta = await readMetaJson(candidate.node.metaPath);
|
|
10371
|
-
freshMeta._generatedAt = new Date().toISOString();
|
|
10372
|
-
await writeFile(join(candidate.node.metaPath, 'meta.json'), JSON.stringify(freshMeta, null, 2));
|
|
10373
|
-
releaseLock(candidate.node.metaPath);
|
|
10374
|
-
if (config.skipUnchanged)
|
|
10375
|
-
continue;
|
|
10376
|
-
return { synthesized: false };
|
|
10377
|
-
}
|
|
10378
|
-
winner = candidate;
|
|
10379
|
-
break;
|
|
10380
|
-
}
|
|
10381
|
-
if (!winner)
|
|
10382
|
-
return { synthesized: false };
|
|
10383
|
-
const node = winner.node;
|
|
10384
|
-
try {
|
|
10385
|
-
const currentMeta = await readMetaJson(node.metaPath);
|
|
10386
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
10387
|
-
}
|
|
10388
|
-
finally {
|
|
10389
|
-
// Step 13: Release lock
|
|
10390
|
-
releaseLock(node.metaPath);
|
|
10391
|
-
}
|
|
10392
|
-
}
|
|
10393
9644
|
/**
|
|
10394
|
-
*
|
|
9645
|
+
* Build a minimal MetaNode for a known meta path.
|
|
10395
9646
|
*
|
|
10396
|
-
*
|
|
10397
|
-
*
|
|
9647
|
+
* Walks the owner directory for child `.meta/meta.json` files and constructs
|
|
9648
|
+
* a shallow ownership tree (self + direct children only).
|
|
10398
9649
|
*
|
|
10399
|
-
* @param
|
|
10400
|
-
* @param
|
|
10401
|
-
* @
|
|
10402
|
-
* @param targetPath - Optional: specific meta/owner path to synthesize instead of stalest candidate.
|
|
10403
|
-
* @returns Array with a single result.
|
|
9650
|
+
* @param metaPath - Absolute path to the `.meta/` directory.
|
|
9651
|
+
* @param watcher - WatcherClient for filesystem enumeration.
|
|
9652
|
+
* @returns MetaNode with direct children wired.
|
|
10404
9653
|
*/
|
|
10405
|
-
async function
|
|
10406
|
-
const
|
|
10407
|
-
|
|
9654
|
+
async function buildMinimalNode(metaPath, watcher) {
|
|
9655
|
+
const normalized = normalizePath(metaPath);
|
|
9656
|
+
const ownerPath = posix.dirname(normalized);
|
|
9657
|
+
// Find child metas using watcher walk.
|
|
9658
|
+
// We include only *direct* children (nearest descendants in the ownership tree)
|
|
9659
|
+
// to match the ownership semantics used elsewhere.
|
|
9660
|
+
const rawMetaJsonPaths = await watcher.walk([
|
|
9661
|
+
`${escapeGlob(ownerPath)}/**/.meta/meta.json`,
|
|
9662
|
+
]);
|
|
9663
|
+
const candidateMetaPaths = [
|
|
9664
|
+
...new Set(rawMetaJsonPaths.map((p) => posix.dirname(normalizePath(p)))),
|
|
9665
|
+
].filter((p) => p !== normalized);
|
|
9666
|
+
const candidates = candidateMetaPaths
|
|
9667
|
+
.map((mp) => ({ metaPath: mp, ownerPath: posix.dirname(mp) }))
|
|
9668
|
+
.sort((a, b) => a.ownerPath.length - b.ownerPath.length);
|
|
9669
|
+
const directChildren = [];
|
|
9670
|
+
for (const c of candidates) {
|
|
9671
|
+
const nestedUnderExisting = directChildren.some((d) => c.ownerPath === d.ownerPath ||
|
|
9672
|
+
c.ownerPath.startsWith(d.ownerPath + '/'));
|
|
9673
|
+
if (!nestedUnderExisting)
|
|
9674
|
+
directChildren.push(c);
|
|
9675
|
+
}
|
|
9676
|
+
const children = directChildren.map((c) => ({
|
|
9677
|
+
metaPath: c.metaPath,
|
|
9678
|
+
ownerPath: c.ownerPath,
|
|
9679
|
+
treeDepth: 1,
|
|
9680
|
+
children: [],
|
|
9681
|
+
parent: null,
|
|
9682
|
+
}));
|
|
9683
|
+
const node = {
|
|
9684
|
+
metaPath: normalized,
|
|
9685
|
+
ownerPath,
|
|
9686
|
+
treeDepth: 0,
|
|
9687
|
+
children,
|
|
9688
|
+
parent: null,
|
|
9689
|
+
};
|
|
9690
|
+
for (const child of children) {
|
|
9691
|
+
child.parent = node;
|
|
9692
|
+
}
|
|
9693
|
+
return node;
|
|
10408
9694
|
}
|
|
10409
9695
|
|
|
10410
9696
|
/**
|
|
@@ -10458,6 +9744,41 @@ function enforceInvariant(state) {
|
|
|
10458
9744
|
}
|
|
10459
9745
|
return result;
|
|
10460
9746
|
}
|
|
9747
|
+
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
9748
|
+
/**
|
|
9749
|
+
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
9750
|
+
* Triggers: _structureHash change, _steer change, _architect change,
|
|
9751
|
+
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
9752
|
+
*/
|
|
9753
|
+
function invalidateArchitect(state) {
|
|
9754
|
+
return enforceInvariant({
|
|
9755
|
+
architect: state.architect === 'failed' ? 'failed' : 'pending',
|
|
9756
|
+
builder: state.builder === 'fresh' ? 'stale' : state.builder,
|
|
9757
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9758
|
+
});
|
|
9759
|
+
}
|
|
9760
|
+
/**
|
|
9761
|
+
* Builder invalidated (scope mtime or cross-ref _content change):
|
|
9762
|
+
* builder → pending; critic → stale.
|
|
9763
|
+
* Only applies when architect is fresh; otherwise, builder stays stale.
|
|
9764
|
+
*/
|
|
9765
|
+
function invalidateBuilder(state) {
|
|
9766
|
+
if (state.architect !== 'fresh') {
|
|
9767
|
+
// Architect is not fresh — builder stays stale (or whatever it is)
|
|
9768
|
+
return enforceInvariant({
|
|
9769
|
+
...state,
|
|
9770
|
+
builder: state.builder === 'fresh' || state.builder === 'stale'
|
|
9771
|
+
? 'stale'
|
|
9772
|
+
: state.builder,
|
|
9773
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9774
|
+
});
|
|
9775
|
+
}
|
|
9776
|
+
return enforceInvariant({
|
|
9777
|
+
...state,
|
|
9778
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
9779
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9780
|
+
});
|
|
9781
|
+
}
|
|
10461
9782
|
// ── Phase success transitions ──────────────────────────────────────────
|
|
10462
9783
|
/**
|
|
10463
9784
|
* Architect completes successfully.
|
|
@@ -10626,7 +9947,9 @@ function derivePhaseState(meta, inputs) {
|
|
|
10626
9947
|
}
|
|
10627
9948
|
// Check architect invalidation (when inputs are provided)
|
|
10628
9949
|
if (inputs) {
|
|
10629
|
-
|
|
9950
|
+
// Progressive metas: structure changes invalidate builder, not architect
|
|
9951
|
+
const structureInvalidatesArchitect = inputs.structureChanged && meta._state === undefined;
|
|
9952
|
+
const architectInvalidated = structureInvalidatesArchitect ||
|
|
10630
9953
|
inputs.steerChanged ||
|
|
10631
9954
|
inputs.architectChanged ||
|
|
10632
9955
|
inputs.crossRefsChanged ||
|
|
@@ -10638,6 +9961,14 @@ function derivePhaseState(meta, inputs) {
|
|
|
10638
9961
|
critic: 'stale',
|
|
10639
9962
|
};
|
|
10640
9963
|
}
|
|
9964
|
+
// Progressive meta with structure change: builder-only invalidation
|
|
9965
|
+
if (inputs.structureChanged && meta._state !== undefined) {
|
|
9966
|
+
return {
|
|
9967
|
+
architect: 'fresh',
|
|
9968
|
+
builder: 'pending',
|
|
9969
|
+
critic: 'stale',
|
|
9970
|
+
};
|
|
9971
|
+
}
|
|
10641
9972
|
}
|
|
10642
9973
|
// Has _builder but no _content: builder is pending
|
|
10643
9974
|
if (meta._builder && !meta._content) {
|
|
@@ -10659,6 +9990,154 @@ function derivePhaseState(meta, inputs) {
|
|
|
10659
9990
|
return freshPhaseState();
|
|
10660
9991
|
}
|
|
10661
9992
|
|
|
9993
|
+
/**
|
|
9994
|
+
* Compute a structure hash from a sorted file listing.
|
|
9995
|
+
*
|
|
9996
|
+
* Used to detect when directory structure changes, triggering
|
|
9997
|
+
* an architect re-run.
|
|
9998
|
+
*
|
|
9999
|
+
* @module structureHash
|
|
10000
|
+
*/
|
|
10001
|
+
/**
|
|
10002
|
+
* Compute a SHA-256 hash of a sorted file listing.
|
|
10003
|
+
*
|
|
10004
|
+
* @param filePaths - Array of file paths in scope.
|
|
10005
|
+
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
10006
|
+
*/
|
|
10007
|
+
function computeStructureHash(filePaths) {
|
|
10008
|
+
const sorted = [...filePaths].sort();
|
|
10009
|
+
const content = sorted.join('\n');
|
|
10010
|
+
return createHash('sha256').update(content).digest('hex');
|
|
10011
|
+
}
|
|
10012
|
+
|
|
10013
|
+
/**
|
|
10014
|
+
* Per-tick invalidation pass.
|
|
10015
|
+
*
|
|
10016
|
+
* Computes architect-invalidating and builder-invalidating inputs for a meta,
|
|
10017
|
+
* then applies the cascade to update _phaseState.
|
|
10018
|
+
*
|
|
10019
|
+
* @module phaseState/invalidate
|
|
10020
|
+
*/
|
|
10021
|
+
/**
|
|
10022
|
+
* Compute invalidation inputs and apply cascade for a single meta.
|
|
10023
|
+
*
|
|
10024
|
+
* @param meta - Current meta.json content with existing _phaseState.
|
|
10025
|
+
* @param scopeFiles - Sorted file list from scope.
|
|
10026
|
+
* @param config - MetaConfig for architectEvery.
|
|
10027
|
+
* @param node - MetaNode for archive access.
|
|
10028
|
+
* @param crossRefMetas - Map of cross-ref owner paths to their current _content.
|
|
10029
|
+
* @param archiveCrossRefContent - Map of cross-ref owner paths to their archived _content.
|
|
10030
|
+
* @returns Updated phase state and invalidation details.
|
|
10031
|
+
*/
|
|
10032
|
+
async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas, archiveCrossRefContent) {
|
|
10033
|
+
let phaseState = meta._phaseState ?? {
|
|
10034
|
+
architect: 'fresh',
|
|
10035
|
+
builder: 'fresh',
|
|
10036
|
+
critic: 'fresh',
|
|
10037
|
+
};
|
|
10038
|
+
// ── Architect-level inputs ──
|
|
10039
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
10040
|
+
const structureChanged = structureHash !== meta._structureHash;
|
|
10041
|
+
const latestArchive = await readLatestArchive(node.metaPath);
|
|
10042
|
+
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10043
|
+
// _architect change: compare current vs. archive
|
|
10044
|
+
const architectChanged = latestArchive
|
|
10045
|
+
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
10046
|
+
: Boolean(meta._architect);
|
|
10047
|
+
// _crossRefs declaration change
|
|
10048
|
+
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
10049
|
+
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
10050
|
+
.slice()
|
|
10051
|
+
.sort()
|
|
10052
|
+
.join(',');
|
|
10053
|
+
const crossRefsDeclChanged = latestArchive
|
|
10054
|
+
? currentRefs !== archiveRefs
|
|
10055
|
+
: currentRefs.length > 0;
|
|
10056
|
+
const architectInvalidators = [];
|
|
10057
|
+
if (structureChanged) {
|
|
10058
|
+
if (meta._state !== undefined) {
|
|
10059
|
+
// Progressive entity: new files → builder only (cursor handles incremental)
|
|
10060
|
+
phaseState = invalidateBuilder(phaseState);
|
|
10061
|
+
}
|
|
10062
|
+
else {
|
|
10063
|
+
architectInvalidators.push('structureHash');
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
if (steerChanged)
|
|
10067
|
+
architectInvalidators.push('steer');
|
|
10068
|
+
if (architectChanged)
|
|
10069
|
+
architectInvalidators.push('_architect');
|
|
10070
|
+
if (crossRefsDeclChanged)
|
|
10071
|
+
architectInvalidators.push('_crossRefs');
|
|
10072
|
+
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
10073
|
+
architectInvalidators.push('architectEvery');
|
|
10074
|
+
}
|
|
10075
|
+
// First-run check: no _builder means architect must run
|
|
10076
|
+
const firstRun = !meta._builder;
|
|
10077
|
+
if (architectInvalidators.length > 0 || firstRun) {
|
|
10078
|
+
phaseState = invalidateArchitect(phaseState);
|
|
10079
|
+
}
|
|
10080
|
+
// ── Builder-level inputs ──
|
|
10081
|
+
// Scope file mtime check — if any file newer than _generatedAt
|
|
10082
|
+
const scopeMtimeMax = null;
|
|
10083
|
+
// Note: actual mtime check is done by the caller or via isStale;
|
|
10084
|
+
// here we just detect cross-ref content changes for the cascade.
|
|
10085
|
+
// Cross-ref _content change (builder-invalidating)
|
|
10086
|
+
let crossRefContentChanged = false;
|
|
10087
|
+
return {
|
|
10088
|
+
phaseState,
|
|
10089
|
+
architectInvalidators,
|
|
10090
|
+
stalenessInputs: {
|
|
10091
|
+
structureHash,
|
|
10092
|
+
steerChanged,
|
|
10093
|
+
architectChanged,
|
|
10094
|
+
crossRefsDeclChanged,
|
|
10095
|
+
scopeMtimeMax,
|
|
10096
|
+
crossRefContentChanged,
|
|
10097
|
+
},
|
|
10098
|
+
structureHash,
|
|
10099
|
+
steerChanged,
|
|
10100
|
+
};
|
|
10101
|
+
}
|
|
10102
|
+
|
|
10103
|
+
/**
|
|
10104
|
+
* Weighted staleness formula for candidate selection.
|
|
10105
|
+
*
|
|
10106
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10107
|
+
*
|
|
10108
|
+
* @module scheduling/weightedFormula
|
|
10109
|
+
*/
|
|
10110
|
+
/**
|
|
10111
|
+
* Compute effective staleness for a set of candidates.
|
|
10112
|
+
*
|
|
10113
|
+
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
10114
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10115
|
+
*
|
|
10116
|
+
* Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
|
|
10117
|
+
* metas to tune how much their tree position affects scheduling.
|
|
10118
|
+
*
|
|
10119
|
+
* @param candidates - Array of \{ node, meta, actualStaleness \}.
|
|
10120
|
+
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
10121
|
+
* @returns Same array with effectiveStaleness computed.
|
|
10122
|
+
*/
|
|
10123
|
+
function computeEffectiveStaleness(candidates, depthWeight) {
|
|
10124
|
+
if (candidates.length === 0)
|
|
10125
|
+
return [];
|
|
10126
|
+
// Get depth for each candidate: use _depth override or tree depth
|
|
10127
|
+
const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
|
|
10128
|
+
// Normalize: shift so minimum becomes 0
|
|
10129
|
+
const minDepth = Math.min(...depths);
|
|
10130
|
+
const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
|
|
10131
|
+
return candidates.map((c, i) => {
|
|
10132
|
+
const emphasis = c.meta._emphasis ?? 1;
|
|
10133
|
+
return {
|
|
10134
|
+
...c,
|
|
10135
|
+
effectiveStaleness: c.actualStaleness *
|
|
10136
|
+
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
10137
|
+
};
|
|
10138
|
+
});
|
|
10139
|
+
}
|
|
10140
|
+
|
|
10662
10141
|
/**
|
|
10663
10142
|
* Corpus-wide phase scheduler.
|
|
10664
10143
|
*
|
|
@@ -10671,18 +10150,30 @@ function derivePhaseState(meta, inputs) {
|
|
|
10671
10150
|
/**
|
|
10672
10151
|
* Build phase candidates from listMetas entries.
|
|
10673
10152
|
*
|
|
10674
|
-
* Derives phase state
|
|
10153
|
+
* Derives phase state, auto-retries failed phases, and applies Tier 1
|
|
10154
|
+
* cheap-invalidation (no I/O) for metas with persisted _phaseState.
|
|
10675
10155
|
* Used by orchestratePhase, queue route, and status route.
|
|
10676
10156
|
*/
|
|
10677
|
-
function buildPhaseCandidates(entries) {
|
|
10678
|
-
return entries.map((entry) =>
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10157
|
+
function buildPhaseCandidates(entries, architectEvery) {
|
|
10158
|
+
return entries.map((entry) => {
|
|
10159
|
+
let ps = retryAllFailed(derivePhaseState(entry.meta));
|
|
10160
|
+
// Tier 1 cheap invalidation for metas with persisted _phaseState
|
|
10161
|
+
if (entry.meta._phaseState) {
|
|
10162
|
+
const needsArchitect = !entry.meta._builder ||
|
|
10163
|
+
(entry.meta._synthesisCount ?? 0) >= architectEvery;
|
|
10164
|
+
if (needsArchitect && ps.architect === 'fresh') {
|
|
10165
|
+
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
10166
|
+
}
|
|
10167
|
+
}
|
|
10168
|
+
return {
|
|
10169
|
+
node: entry.node,
|
|
10170
|
+
meta: entry.meta,
|
|
10171
|
+
phaseState: ps,
|
|
10172
|
+
actualStaleness: entry.stalenessSeconds,
|
|
10173
|
+
locked: entry.locked,
|
|
10174
|
+
disabled: entry.disabled,
|
|
10175
|
+
};
|
|
10176
|
+
});
|
|
10686
10177
|
}
|
|
10687
10178
|
/**
|
|
10688
10179
|
* Rank all eligible phase candidates by priority.
|
|
@@ -10735,14 +10226,132 @@ function rankPhaseCandidates(metas, depthWeight) {
|
|
|
10735
10226
|
return candidates;
|
|
10736
10227
|
}
|
|
10737
10228
|
/**
|
|
10738
|
-
* Select the best phase candidate across the corpus.
|
|
10229
|
+
* Select the best phase candidate across the corpus.
|
|
10230
|
+
*
|
|
10231
|
+
* @param metas - Array of (node, meta, phaseState, stalenessSeconds) tuples.
|
|
10232
|
+
* @param depthWeight - Config depthWeight for staleness tiebreak.
|
|
10233
|
+
* @returns The winning candidate, or null if no phase is ready.
|
|
10234
|
+
*/
|
|
10235
|
+
function selectPhaseCandidate(metas, depthWeight) {
|
|
10236
|
+
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
10237
|
+
}
|
|
10238
|
+
|
|
10239
|
+
/**
|
|
10240
|
+
* Shared error utilities.
|
|
10241
|
+
*
|
|
10242
|
+
* @module errors
|
|
10243
|
+
*/
|
|
10244
|
+
/**
|
|
10245
|
+
* Wrap an unknown caught value into a MetaError.
|
|
10246
|
+
*
|
|
10247
|
+
* @param step - Which synthesis step failed.
|
|
10248
|
+
* @param err - The caught error value.
|
|
10249
|
+
* @param code - Error classification code.
|
|
10250
|
+
* @returns A structured MetaError.
|
|
10251
|
+
*/
|
|
10252
|
+
function toMetaError(step, err, code = 'FAILED') {
|
|
10253
|
+
return {
|
|
10254
|
+
step,
|
|
10255
|
+
code,
|
|
10256
|
+
message: err instanceof Error ? err.message : String(err),
|
|
10257
|
+
};
|
|
10258
|
+
}
|
|
10259
|
+
|
|
10260
|
+
/**
|
|
10261
|
+
* Parse subprocess outputs for each synthesis step.
|
|
10262
|
+
*
|
|
10263
|
+
* - Architect: returns text \> _builder
|
|
10264
|
+
* - Builder: returns JSON \> _content + structured fields
|
|
10265
|
+
* - Critic: returns text \> _feedback
|
|
10266
|
+
*
|
|
10267
|
+
* @module orchestrator/parseOutput
|
|
10268
|
+
*/
|
|
10269
|
+
/**
|
|
10270
|
+
* Parse architect output. The architect returns a task brief as text.
|
|
10271
|
+
*
|
|
10272
|
+
* @param output - Raw subprocess output.
|
|
10273
|
+
* @returns The task brief string.
|
|
10274
|
+
*/
|
|
10275
|
+
function parseArchitectOutput(output) {
|
|
10276
|
+
return output.trim();
|
|
10277
|
+
}
|
|
10278
|
+
/**
|
|
10279
|
+
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
10280
|
+
*
|
|
10281
|
+
* Attempts JSON parse first. If that fails, treats the entire output as _content.
|
|
10282
|
+
*
|
|
10283
|
+
* @param output - Raw subprocess output.
|
|
10284
|
+
* @returns Parsed builder output with content and structured fields.
|
|
10285
|
+
*/
|
|
10286
|
+
function parseBuilderOutput(output) {
|
|
10287
|
+
const trimmed = output.trim();
|
|
10288
|
+
// Strategy 1: Try to parse the entire output as JSON directly
|
|
10289
|
+
const direct = tryParseJson(trimmed);
|
|
10290
|
+
if (direct)
|
|
10291
|
+
return direct;
|
|
10292
|
+
// Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
|
|
10293
|
+
const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
10294
|
+
const fenceMatches = [];
|
|
10295
|
+
let match;
|
|
10296
|
+
while ((match = fencePattern.exec(trimmed)) !== null) {
|
|
10297
|
+
fenceMatches.push(match[1].trim());
|
|
10298
|
+
}
|
|
10299
|
+
// Try last fence first (most likely to be the actual output)
|
|
10300
|
+
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
10301
|
+
const result = tryParseJson(fenceMatches[i]);
|
|
10302
|
+
if (result)
|
|
10303
|
+
return result;
|
|
10304
|
+
}
|
|
10305
|
+
// Strategy 3: Find outermost { ... } braces
|
|
10306
|
+
const firstBrace = trimmed.indexOf('{');
|
|
10307
|
+
const lastBrace = trimmed.lastIndexOf('}');
|
|
10308
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
10309
|
+
const result = tryParseJson(trimmed.substring(firstBrace, lastBrace + 1));
|
|
10310
|
+
if (result)
|
|
10311
|
+
return result;
|
|
10312
|
+
}
|
|
10313
|
+
// Fallback: treat entire output as content
|
|
10314
|
+
return { content: trimmed, fields: {} };
|
|
10315
|
+
}
|
|
10316
|
+
/** Try to parse a string as JSON and extract builder output fields. */
|
|
10317
|
+
function tryParseJson(str) {
|
|
10318
|
+
try {
|
|
10319
|
+
const raw = JSON.parse(str);
|
|
10320
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
10321
|
+
return null;
|
|
10322
|
+
}
|
|
10323
|
+
const parsed = raw;
|
|
10324
|
+
// Extract _content
|
|
10325
|
+
const content = typeof parsed['_content'] === 'string'
|
|
10326
|
+
? parsed['_content']
|
|
10327
|
+
: typeof parsed['content'] === 'string'
|
|
10328
|
+
? parsed['content']
|
|
10329
|
+
: null;
|
|
10330
|
+
if (content === null)
|
|
10331
|
+
return null;
|
|
10332
|
+
// Extract _state (the ONLY underscore key the builder is allowed to set)
|
|
10333
|
+
const state = '_state' in parsed ? parsed['_state'] : undefined;
|
|
10334
|
+
// Extract non-underscore fields
|
|
10335
|
+
const fields = {};
|
|
10336
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
10337
|
+
if (!key.startsWith('_') && key !== 'content') {
|
|
10338
|
+
fields[key] = value;
|
|
10339
|
+
}
|
|
10340
|
+
}
|
|
10341
|
+
return { content, fields, ...(state !== undefined ? { state } : {}) };
|
|
10342
|
+
}
|
|
10343
|
+
catch {
|
|
10344
|
+
return null;
|
|
10345
|
+
}
|
|
10346
|
+
}
|
|
10347
|
+
/**
|
|
10348
|
+
* Parse critic output. The critic returns evaluation text.
|
|
10739
10349
|
*
|
|
10740
|
-
* @param
|
|
10741
|
-
* @
|
|
10742
|
-
* @returns The winning candidate, or null if no phase is ready.
|
|
10350
|
+
* @param output - Raw subprocess output.
|
|
10351
|
+
* @returns The feedback string.
|
|
10743
10352
|
*/
|
|
10744
|
-
function
|
|
10745
|
-
return
|
|
10353
|
+
function parseCriticOutput(output) {
|
|
10354
|
+
return output.trim();
|
|
10746
10355
|
}
|
|
10747
10356
|
|
|
10748
10357
|
/**
|
|
@@ -11003,7 +10612,7 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
|
|
|
11003
10612
|
if (metaResult.entries.length === 0)
|
|
11004
10613
|
return { executed: false };
|
|
11005
10614
|
// Build candidates with phase state (including invalidation + auto-retry)
|
|
11006
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
10615
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
11007
10616
|
// Select best phase candidate
|
|
11008
10617
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
11009
10618
|
if (!winner) {
|
|
@@ -11678,46 +11287,6 @@ function buildMetaRules(config) {
|
|
|
11678
11287
|
},
|
|
11679
11288
|
renderAs: 'md',
|
|
11680
11289
|
},
|
|
11681
|
-
{
|
|
11682
|
-
name: 'meta-config',
|
|
11683
|
-
description: 'jeeves-meta configuration file',
|
|
11684
|
-
match: {
|
|
11685
|
-
properties: {
|
|
11686
|
-
file: {
|
|
11687
|
-
properties: {
|
|
11688
|
-
path: {
|
|
11689
|
-
type: 'string',
|
|
11690
|
-
glob: '**/jeeves-meta{.config.json,/config.json}',
|
|
11691
|
-
},
|
|
11692
|
-
},
|
|
11693
|
-
},
|
|
11694
|
-
},
|
|
11695
|
-
},
|
|
11696
|
-
schema: ['base', { properties: { domains: { set: ['meta-config'] } } }],
|
|
11697
|
-
render: {
|
|
11698
|
-
frontmatter: [
|
|
11699
|
-
'watcherUrl',
|
|
11700
|
-
'gatewayUrl',
|
|
11701
|
-
'architectEvery',
|
|
11702
|
-
'depthWeight',
|
|
11703
|
-
'maxArchive',
|
|
11704
|
-
'maxLines',
|
|
11705
|
-
],
|
|
11706
|
-
body: [
|
|
11707
|
-
{
|
|
11708
|
-
path: 'json.defaultArchitect',
|
|
11709
|
-
heading: 2,
|
|
11710
|
-
label: 'Default Architect Prompt',
|
|
11711
|
-
},
|
|
11712
|
-
{
|
|
11713
|
-
path: 'json.defaultCritic',
|
|
11714
|
-
heading: 2,
|
|
11715
|
-
label: 'Default Critic Prompt',
|
|
11716
|
-
},
|
|
11717
|
-
],
|
|
11718
|
-
},
|
|
11719
|
-
renderAs: 'md',
|
|
11720
|
-
},
|
|
11721
11290
|
];
|
|
11722
11291
|
}
|
|
11723
11292
|
/**
|
|
@@ -11961,13 +11530,15 @@ class Scheduler {
|
|
|
11961
11530
|
queue;
|
|
11962
11531
|
logger;
|
|
11963
11532
|
watcher;
|
|
11533
|
+
cache;
|
|
11964
11534
|
registrar = null;
|
|
11965
11535
|
currentExpression;
|
|
11966
|
-
constructor(config, queue, logger, watcher) {
|
|
11536
|
+
constructor(config, queue, logger, watcher, cache) {
|
|
11967
11537
|
this.config = config;
|
|
11968
11538
|
this.queue = queue;
|
|
11969
11539
|
this.logger = logger;
|
|
11970
11540
|
this.watcher = watcher;
|
|
11541
|
+
this.cache = cache;
|
|
11971
11542
|
this.currentExpression = config.schedule;
|
|
11972
11543
|
}
|
|
11973
11544
|
/** Set the rule registrar for watcher restart detection. */
|
|
@@ -12084,8 +11655,8 @@ class Scheduler {
|
|
|
12084
11655
|
*/
|
|
12085
11656
|
async discoverNextPhase() {
|
|
12086
11657
|
try {
|
|
12087
|
-
const result = await
|
|
12088
|
-
const candidates = buildPhaseCandidates(result.entries);
|
|
11658
|
+
const result = await this.cache.get(this.config, this.watcher);
|
|
11659
|
+
const candidates = buildPhaseCandidates(result.entries, this.config.architectEvery);
|
|
12089
11660
|
const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
|
|
12090
11661
|
if (!winner)
|
|
12091
11662
|
return null;
|
|
@@ -12510,11 +12081,11 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12510
12081
|
*/
|
|
12511
12082
|
function registerPreviewRoute(app, deps) {
|
|
12512
12083
|
app.get('/preview', async (request, reply) => {
|
|
12513
|
-
const { config, watcher } = deps;
|
|
12084
|
+
const { config, watcher, cache } = deps;
|
|
12514
12085
|
const query = request.query;
|
|
12515
12086
|
let result;
|
|
12516
12087
|
try {
|
|
12517
|
-
result = await
|
|
12088
|
+
result = await cache.get(config, watcher);
|
|
12518
12089
|
}
|
|
12519
12090
|
catch {
|
|
12520
12091
|
return reply.status(503).send({
|
|
@@ -12534,40 +12105,24 @@ function registerPreviewRoute(app, deps) {
|
|
|
12534
12105
|
}
|
|
12535
12106
|
}
|
|
12536
12107
|
else {
|
|
12537
|
-
// Select
|
|
12538
|
-
const
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
node: e.node,
|
|
12542
|
-
meta: e.meta,
|
|
12543
|
-
actualStaleness: e.stalenessSeconds,
|
|
12544
|
-
}));
|
|
12545
|
-
const stalestPath = discoverStalestPath(stale, config.depthWeight);
|
|
12546
|
-
if (!stalestPath) {
|
|
12108
|
+
// Select best phase candidate
|
|
12109
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
12110
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12111
|
+
if (!winner) {
|
|
12547
12112
|
return { message: 'No stale metas found. Nothing to synthesize.' };
|
|
12548
12113
|
}
|
|
12549
|
-
targetNode = findNode(result.tree,
|
|
12114
|
+
targetNode = findNode(result.tree, winner.node.metaPath);
|
|
12550
12115
|
}
|
|
12551
12116
|
const meta = await readMetaJson(targetNode.metaPath);
|
|
12552
12117
|
// Scope files
|
|
12553
12118
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
12554
|
-
|
|
12119
|
+
// Compute invalidation inputs (DRY: reuse phaseState/invalidate logic)
|
|
12120
|
+
const invalidation = await computeInvalidation(meta, scopeFiles, config, targetNode);
|
|
12121
|
+
const { architectInvalidators, stalenessInputs } = invalidation;
|
|
12122
|
+
const { structureHash } = invalidation;
|
|
12555
12123
|
const structureChanged = structureHash !== meta._structureHash;
|
|
12556
|
-
const
|
|
12557
|
-
const
|
|
12558
|
-
// _architect change detection
|
|
12559
|
-
const architectChanged = latestArchive
|
|
12560
|
-
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
12561
|
-
: Boolean(meta._architect);
|
|
12562
|
-
// _crossRefs declaration change detection
|
|
12563
|
-
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
12564
|
-
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
12565
|
-
.slice()
|
|
12566
|
-
.sort()
|
|
12567
|
-
.join(',');
|
|
12568
|
-
const crossRefsDeclChanged = latestArchive
|
|
12569
|
-
? currentRefs !== archiveRefs
|
|
12570
|
-
: currentRefs.length > 0;
|
|
12124
|
+
const { steerChanged } = invalidation;
|
|
12125
|
+
const { architectChanged, crossRefsDeclChanged } = stalenessInputs;
|
|
12571
12126
|
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
12572
12127
|
// Delta files
|
|
12573
12128
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
@@ -12592,30 +12147,6 @@ function registerPreviewRoute(app, deps) {
|
|
|
12592
12147
|
});
|
|
12593
12148
|
const owedPhase = getOwedPhase(phaseState);
|
|
12594
12149
|
const priorityBand = getPriorityBand(phaseState);
|
|
12595
|
-
// Architect invalidators
|
|
12596
|
-
const architectInvalidators = [];
|
|
12597
|
-
if (owedPhase === 'architect') {
|
|
12598
|
-
if (structureChanged)
|
|
12599
|
-
architectInvalidators.push('structureHash');
|
|
12600
|
-
if (steerChanged)
|
|
12601
|
-
architectInvalidators.push('steer');
|
|
12602
|
-
if (architectChanged)
|
|
12603
|
-
architectInvalidators.push('_architect');
|
|
12604
|
-
if (crossRefsDeclChanged)
|
|
12605
|
-
architectInvalidators.push('_crossRefs');
|
|
12606
|
-
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
12607
|
-
architectInvalidators.push('architectEvery');
|
|
12608
|
-
}
|
|
12609
|
-
}
|
|
12610
|
-
// Staleness inputs
|
|
12611
|
-
const stalenessInputs = {
|
|
12612
|
-
structureHash,
|
|
12613
|
-
steerChanged,
|
|
12614
|
-
architectChanged,
|
|
12615
|
-
crossRefsDeclChanged,
|
|
12616
|
-
scopeMtimeMax: null,
|
|
12617
|
-
crossRefContentChanged: false,
|
|
12618
|
-
};
|
|
12619
12150
|
return {
|
|
12620
12151
|
path: targetNode.metaPath,
|
|
12621
12152
|
staleness: {
|
|
@@ -12689,8 +12220,8 @@ function registerQueueRoutes(app, deps) {
|
|
|
12689
12220
|
// ranked by scheduler priority (computed on read, not persisted)
|
|
12690
12221
|
let automatic = [];
|
|
12691
12222
|
try {
|
|
12692
|
-
const metaResult = await
|
|
12693
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12223
|
+
const metaResult = await deps.cache.get(deps.config, deps.watcher);
|
|
12224
|
+
const candidates = buildPhaseCandidates(metaResult.entries, deps.config.architectEvery);
|
|
12694
12225
|
const ranked = rankPhaseCandidates(candidates, deps.config.depthWeight);
|
|
12695
12226
|
automatic = ranked.map((c) => ({
|
|
12696
12227
|
path: c.node.metaPath,
|
|
@@ -12885,7 +12416,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12885
12416
|
name: SERVICE_NAME,
|
|
12886
12417
|
version: SERVICE_VERSION,
|
|
12887
12418
|
getHealth: async () => {
|
|
12888
|
-
const { config, queue, scheduler, stats, watcher } = deps;
|
|
12419
|
+
const { config, queue, scheduler, stats, watcher, cache } = deps;
|
|
12889
12420
|
// On-demand dependency checks
|
|
12890
12421
|
const [watcherHealth, gatewayHealth] = await Promise.all([
|
|
12891
12422
|
checkWatcher(config.watcherUrl),
|
|
@@ -12899,7 +12430,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12899
12430
|
};
|
|
12900
12431
|
let nextPhase = null;
|
|
12901
12432
|
try {
|
|
12902
|
-
const metaResult = await
|
|
12433
|
+
const metaResult = await cache.get(config, watcher);
|
|
12903
12434
|
// Count raw phase states (before retry) for display
|
|
12904
12435
|
for (const entry of metaResult.entries) {
|
|
12905
12436
|
const ps = derivePhaseState(entry.meta);
|
|
@@ -12908,7 +12439,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12908
12439
|
}
|
|
12909
12440
|
}
|
|
12910
12441
|
// Build candidates (with auto-retry) for scheduling
|
|
12911
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12442
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
12912
12443
|
// Find next phase candidate
|
|
12913
12444
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12914
12445
|
if (winner) {
|
|
@@ -12971,7 +12502,7 @@ const synthesizeBodySchema = z.object({
|
|
|
12971
12502
|
function registerSynthesizeRoute(app, deps) {
|
|
12972
12503
|
app.post('/synthesize', async (request, reply) => {
|
|
12973
12504
|
const body = synthesizeBodySchema.parse(request.body);
|
|
12974
|
-
const { config, watcher, queue } = deps;
|
|
12505
|
+
const { config, watcher, queue, cache } = deps;
|
|
12975
12506
|
if (body.path) {
|
|
12976
12507
|
// Path-targeted trigger: create override entry
|
|
12977
12508
|
const targetPath = resolveMetaDir(body.path);
|
|
@@ -13008,7 +12539,7 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
13008
12539
|
// Corpus-wide trigger: discover stalest candidate
|
|
13009
12540
|
let result;
|
|
13010
12541
|
try {
|
|
13011
|
-
result = await
|
|
12542
|
+
result = await cache.get(config, watcher);
|
|
13012
12543
|
}
|
|
13013
12544
|
catch {
|
|
13014
12545
|
return reply.status(503).send({
|
|
@@ -13016,20 +12547,15 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
13016
12547
|
message: 'Watcher unreachable — cannot discover candidates',
|
|
13017
12548
|
});
|
|
13018
12549
|
}
|
|
13019
|
-
const
|
|
13020
|
-
|
|
13021
|
-
|
|
13022
|
-
node: e.node,
|
|
13023
|
-
meta: e.meta,
|
|
13024
|
-
actualStaleness: e.stalenessSeconds,
|
|
13025
|
-
}));
|
|
13026
|
-
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
13027
|
-
if (!stalest) {
|
|
12550
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
12551
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12552
|
+
if (!winner) {
|
|
13028
12553
|
return reply.code(200).send({
|
|
13029
12554
|
status: 'skipped',
|
|
13030
12555
|
message: 'No stale metas found. Nothing to synthesize.',
|
|
13031
12556
|
});
|
|
13032
12557
|
}
|
|
12558
|
+
const stalest = winner.node.metaPath;
|
|
13033
12559
|
const enqueueResult = queue.enqueue(stalest);
|
|
13034
12560
|
return reply.code(202).send({
|
|
13035
12561
|
status: 'accepted',
|
|
@@ -13130,6 +12656,18 @@ function createServer(options) {
|
|
|
13130
12656
|
// Fastify 5 requires `loggerInstance` for external pino loggers
|
|
13131
12657
|
const app = Fastify({
|
|
13132
12658
|
loggerInstance: options.logger,
|
|
12659
|
+
requestTimeout: 30_000,
|
|
12660
|
+
});
|
|
12661
|
+
// Readiness gate: return 503 while service is initializing
|
|
12662
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
12663
|
+
if (options.deps.ready)
|
|
12664
|
+
return;
|
|
12665
|
+
const url = request.url;
|
|
12666
|
+
if (url === '/config' || url.startsWith('/config/apply'))
|
|
12667
|
+
return;
|
|
12668
|
+
return reply
|
|
12669
|
+
.status(503)
|
|
12670
|
+
.send({ status: 'starting', message: 'Service initializing' });
|
|
13133
12671
|
});
|
|
13134
12672
|
registerRoutes(app, options.deps);
|
|
13135
12673
|
return app;
|
|
@@ -13305,8 +12843,9 @@ async function startService(config, configPath) {
|
|
|
13305
12843
|
lastCycleAt: null,
|
|
13306
12844
|
};
|
|
13307
12845
|
const queue = new SynthesisQueue(logger);
|
|
12846
|
+
const cache = new MetaCache();
|
|
13308
12847
|
// Scheduler (needs watcher for discovery)
|
|
13309
|
-
const scheduler = new Scheduler(config, queue, logger, watcher);
|
|
12848
|
+
const scheduler = new Scheduler(config, queue, logger, watcher, cache);
|
|
13310
12849
|
const routeDeps = {
|
|
13311
12850
|
config,
|
|
13312
12851
|
logger,
|
|
@@ -13314,6 +12853,8 @@ async function startService(config, configPath) {
|
|
|
13314
12853
|
watcher,
|
|
13315
12854
|
scheduler,
|
|
13316
12855
|
stats,
|
|
12856
|
+
cache,
|
|
12857
|
+
ready: false,
|
|
13317
12858
|
executor,
|
|
13318
12859
|
configPath,
|
|
13319
12860
|
};
|
|
@@ -13364,6 +12905,9 @@ async function startService(config, configPath) {
|
|
|
13364
12905
|
}
|
|
13365
12906
|
await progress.report(evt);
|
|
13366
12907
|
}, logger);
|
|
12908
|
+
// Invalidate cache only when a phase was actually executed
|
|
12909
|
+
if (result.executed)
|
|
12910
|
+
cache.invalidate();
|
|
13367
12911
|
const durationMs = Date.now() - startMs;
|
|
13368
12912
|
if (!result.executed) {
|
|
13369
12913
|
logger.debug({ path: ownerPath }, 'Phase skipped (fully fresh or locked)');
|
|
@@ -13427,9 +12971,13 @@ async function startService(config, configPath) {
|
|
|
13427
12971
|
scheduler.setRegistrar(registrar);
|
|
13428
12972
|
routeDeps.registrar = registrar;
|
|
13429
12973
|
void registrar.register().then(() => {
|
|
12974
|
+
routeDeps.ready = true;
|
|
13430
12975
|
if (registrar.isRegistered) {
|
|
13431
12976
|
void verifyRuleApplication(watcher, logger);
|
|
13432
12977
|
}
|
|
12978
|
+
}, () => {
|
|
12979
|
+
// Registration failed after max retries — mark ready anyway
|
|
12980
|
+
routeDeps.ready = true;
|
|
13433
12981
|
});
|
|
13434
12982
|
// Periodic watcher health check (independent of scheduler)
|
|
13435
12983
|
const healthCheck = new WatcherHealthCheck({
|
|
@@ -13511,4 +13059,152 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
13511
13059
|
customCliCommands: registerCustomCliCommands,
|
|
13512
13060
|
});
|
|
13513
13061
|
|
|
13514
|
-
|
|
13062
|
+
/**
|
|
13063
|
+
* Exponential moving average helper for token tracking.
|
|
13064
|
+
*
|
|
13065
|
+
* @module ema
|
|
13066
|
+
*/
|
|
13067
|
+
const DEFAULT_DECAY = 0.3;
|
|
13068
|
+
/**
|
|
13069
|
+
* Compute exponential moving average.
|
|
13070
|
+
*
|
|
13071
|
+
* @param current - New observation.
|
|
13072
|
+
* @param previous - Previous EMA value, or undefined for first observation.
|
|
13073
|
+
* @param decay - Decay factor (0-1). Higher = more weight on new value. Default 0.3.
|
|
13074
|
+
* @returns Updated EMA.
|
|
13075
|
+
*/
|
|
13076
|
+
function computeEma(current, previous, decay = DEFAULT_DECAY) {
|
|
13077
|
+
if (previous === undefined)
|
|
13078
|
+
return current;
|
|
13079
|
+
return decay * current + (1 - decay) * previous;
|
|
13080
|
+
}
|
|
13081
|
+
|
|
13082
|
+
/**
|
|
13083
|
+
* Structured error from a synthesis step failure.
|
|
13084
|
+
*
|
|
13085
|
+
* @module schema/error
|
|
13086
|
+
*/
|
|
13087
|
+
/** Zod schema for synthesis step errors. */
|
|
13088
|
+
const metaErrorSchema = z.object({
|
|
13089
|
+
/** Which step failed: 'architect', 'builder', or 'critic'. */
|
|
13090
|
+
step: z.enum(['architect', 'builder', 'critic']),
|
|
13091
|
+
/** Error classification code. */
|
|
13092
|
+
code: z.string(),
|
|
13093
|
+
/** Human-readable error message. */
|
|
13094
|
+
message: z.string(),
|
|
13095
|
+
});
|
|
13096
|
+
|
|
13097
|
+
/**
|
|
13098
|
+
* Zod schema for .meta/meta.json files.
|
|
13099
|
+
*
|
|
13100
|
+
* Reserved properties are underscore-prefixed and engine-managed.
|
|
13101
|
+
* All other keys are open schema (builder output).
|
|
13102
|
+
*
|
|
13103
|
+
* @module schema/meta
|
|
13104
|
+
*/
|
|
13105
|
+
/** Valid states for a synthesis phase. */
|
|
13106
|
+
const phaseStatuses = [
|
|
13107
|
+
'fresh',
|
|
13108
|
+
'stale',
|
|
13109
|
+
'pending',
|
|
13110
|
+
'running',
|
|
13111
|
+
'failed',
|
|
13112
|
+
];
|
|
13113
|
+
/** Zod schema for a per-phase status value. */
|
|
13114
|
+
const phaseStatusSchema = z.enum(phaseStatuses);
|
|
13115
|
+
/** Zod schema for the per-meta phase state record. */
|
|
13116
|
+
const phaseStateSchema = z.object({
|
|
13117
|
+
architect: phaseStatusSchema,
|
|
13118
|
+
builder: phaseStatusSchema,
|
|
13119
|
+
critic: phaseStatusSchema,
|
|
13120
|
+
});
|
|
13121
|
+
/** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
|
|
13122
|
+
const metaJsonSchema = z
|
|
13123
|
+
.object({
|
|
13124
|
+
/** Stable identity. Auto-generated on first synthesis if not provided. */
|
|
13125
|
+
_id: z.uuid().optional(),
|
|
13126
|
+
/** Human-provided steering prompt. Optional. */
|
|
13127
|
+
_steer: z.string().optional(),
|
|
13128
|
+
/**
|
|
13129
|
+
* Explicit cross-references to other meta owner paths.
|
|
13130
|
+
* Referenced metas' _content is included as architect/builder context.
|
|
13131
|
+
*/
|
|
13132
|
+
_crossRefs: z.array(z.string()).optional(),
|
|
13133
|
+
/** Architect system prompt used this turn. Defaults from config. */
|
|
13134
|
+
_architect: z.string().optional(),
|
|
13135
|
+
/**
|
|
13136
|
+
* Task brief generated by the architect. Cached and reused across cycles;
|
|
13137
|
+
* regenerated only when triggered.
|
|
13138
|
+
*/
|
|
13139
|
+
_builder: z.string().optional(),
|
|
13140
|
+
/** Critic system prompt used this turn. Defaults from config. */
|
|
13141
|
+
_critic: z.string().optional(),
|
|
13142
|
+
/** Timestamp of last synthesis. ISO 8601. */
|
|
13143
|
+
_generatedAt: z.iso.datetime().optional(),
|
|
13144
|
+
/** Narrative synthesis output. Rendered by watcher for embedding. */
|
|
13145
|
+
_content: z.string().optional(),
|
|
13146
|
+
/**
|
|
13147
|
+
* Hash of sorted file listing in scope. Detects directory structure
|
|
13148
|
+
* changes that trigger an architect re-run.
|
|
13149
|
+
*/
|
|
13150
|
+
_structureHash: z.string().optional(),
|
|
13151
|
+
/**
|
|
13152
|
+
* Cycles since last architect run. Reset to 0 when architect runs.
|
|
13153
|
+
* Used with architectEvery to trigger periodic re-prompting.
|
|
13154
|
+
*/
|
|
13155
|
+
_synthesisCount: z.number().int().min(0).optional(),
|
|
13156
|
+
/** Critic evaluation of the last synthesis. */
|
|
13157
|
+
_feedback: z.string().optional(),
|
|
13158
|
+
/**
|
|
13159
|
+
* Present and true on archive snapshots. Distinguishes live vs. archived
|
|
13160
|
+
* metas.
|
|
13161
|
+
*/
|
|
13162
|
+
_archived: z.boolean().optional(),
|
|
13163
|
+
/** Timestamp when this snapshot was archived. ISO 8601. */
|
|
13164
|
+
_archivedAt: z.iso.datetime().optional(),
|
|
13165
|
+
/**
|
|
13166
|
+
* Scheduling priority. Higher = updates more often. Negative allowed;
|
|
13167
|
+
* normalized to min 0 at scheduling time.
|
|
13168
|
+
*/
|
|
13169
|
+
_depth: z.number().optional(),
|
|
13170
|
+
/**
|
|
13171
|
+
* Emphasis multiplier for depth weighting in scheduling.
|
|
13172
|
+
* Default 1. Higher values increase this meta's scheduling priority
|
|
13173
|
+
* relative to its depth. Set to 0.5 to halve the depth effect,
|
|
13174
|
+
* 2 to double it, 0 to ignore depth entirely for this meta.
|
|
13175
|
+
*/
|
|
13176
|
+
_emphasis: z.number().min(0).optional(),
|
|
13177
|
+
/** Token count from last architect subprocess call. */
|
|
13178
|
+
_architectTokens: z.number().int().optional(),
|
|
13179
|
+
/** Token count from last builder subprocess call. */
|
|
13180
|
+
_builderTokens: z.number().int().optional(),
|
|
13181
|
+
/** Token count from last critic subprocess call. */
|
|
13182
|
+
_criticTokens: z.number().int().optional(),
|
|
13183
|
+
/** Exponential moving average of architect token usage (decay 0.3). */
|
|
13184
|
+
_architectTokensAvg: z.number().optional(),
|
|
13185
|
+
/** Exponential moving average of builder token usage (decay 0.3). */
|
|
13186
|
+
_builderTokensAvg: z.number().optional(),
|
|
13187
|
+
/** Exponential moving average of critic token usage (decay 0.3). */
|
|
13188
|
+
_criticTokensAvg: z.number().optional(),
|
|
13189
|
+
/**
|
|
13190
|
+
* Opaque state carried across synthesis cycles for progressive work.
|
|
13191
|
+
* Set by the builder, passed back as context on next cycle.
|
|
13192
|
+
*/
|
|
13193
|
+
_state: z.unknown().optional(),
|
|
13194
|
+
/**
|
|
13195
|
+
* Structured error from last cycle. Present when a step failed.
|
|
13196
|
+
* Cleared on successful cycle.
|
|
13197
|
+
*/
|
|
13198
|
+
_error: metaErrorSchema.optional(),
|
|
13199
|
+
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
13200
|
+
_disabled: z.boolean().optional(),
|
|
13201
|
+
/**
|
|
13202
|
+
* Per-phase state machine record. Engine-managed.
|
|
13203
|
+
* Keyed by phase name (architect, builder, critic) with status values.
|
|
13204
|
+
* Persisted to survive ticks; derived on first load for back-compat.
|
|
13205
|
+
*/
|
|
13206
|
+
_phaseState: phaseStateSchema.optional(),
|
|
13207
|
+
})
|
|
13208
|
+
.loose();
|
|
13209
|
+
|
|
13210
|
+
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, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestratePhase, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, runArchitect, runBuilder, runCritic, serviceConfigSchema, sleepAsync as sleep, startService, toMetaError, verifyRuleApplication };
|