@karmaniverous/jeeves-meta 0.15.7 → 0.15.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli/jeeves-meta/index.js +465 -254
- package/dist/discovery/getAncestorMeta.d.ts +16 -0
- package/dist/discovery/index.d.ts +1 -0
- package/dist/index.js +462 -239
- package/dist/interfaces/MetaContext.d.ts +2 -0
- package/dist/phaseState/derivePhaseState.d.ts +0 -2
- package/dist/phaseState/index.d.ts +2 -2
- package/dist/phaseState/invalidate.d.ts +5 -7
- package/dist/phaseState/phaseScheduler.d.ts +5 -0
- package/dist/phaseState/phaseTransitions.d.ts +1 -1
- package/dist/routes/metasUpdate.d.ts +1 -1
- package/dist/routes/status.d.ts +1 -1
- package/dist/rules/index.d.ts +1 -3
- package/dist/scheduler/index.d.ts +5 -0
- package/dist/schema/config.d.ts +9 -0
- package/dist/schema/meta.d.ts +4 -0
- package/dist/seed/createMeta.d.ts +6 -0
- package/package.json +58 -58
|
@@ -5615,6 +5615,9 @@ function requireRange () {
|
|
|
5615
5615
|
}
|
|
5616
5616
|
|
|
5617
5617
|
parseRange (range) {
|
|
5618
|
+
// strip build metadata so it can't bleed into the version
|
|
5619
|
+
range = range.replace(BUILDSTRIPRE, '');
|
|
5620
|
+
|
|
5618
5621
|
// memoize range parsing for performance.
|
|
5619
5622
|
// this is a very hot path, and fully deterministic.
|
|
5620
5623
|
const memoOpts =
|
|
@@ -5740,6 +5743,7 @@ function requireRange () {
|
|
|
5740
5743
|
const SemVer = requireSemver$1();
|
|
5741
5744
|
const {
|
|
5742
5745
|
safeRe: re,
|
|
5746
|
+
src,
|
|
5743
5747
|
t,
|
|
5744
5748
|
comparatorTrimReplace,
|
|
5745
5749
|
tildeTrimReplace,
|
|
@@ -5747,6 +5751,9 @@ function requireRange () {
|
|
|
5747
5751
|
} = requireRe();
|
|
5748
5752
|
const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = requireConstants();
|
|
5749
5753
|
|
|
5754
|
+
// unbounded global build-metadata stripper used by parseRange
|
|
5755
|
+
const BUILDSTRIPRE = new RegExp(src[t.BUILD], 'g');
|
|
5756
|
+
|
|
5750
5757
|
const isNullSet = c => c.value === '<0.0.0-0';
|
|
5751
5758
|
const isAny = c => c.value === '';
|
|
5752
5759
|
|
|
@@ -6798,7 +6805,7 @@ function requireSubset () {
|
|
|
6798
6805
|
if (higher === c && higher !== gt) {
|
|
6799
6806
|
return false
|
|
6800
6807
|
}
|
|
6801
|
-
} else if (gt.operator === '>=' && !
|
|
6808
|
+
} else if (gt.operator === '>=' && !c.test(gt.semver)) {
|
|
6802
6809
|
return false
|
|
6803
6810
|
}
|
|
6804
6811
|
}
|
|
@@ -6816,7 +6823,7 @@ function requireSubset () {
|
|
|
6816
6823
|
if (lower === c && lower !== lt) {
|
|
6817
6824
|
return false
|
|
6818
6825
|
}
|
|
6819
|
-
} else if (lt.operator === '<=' && !
|
|
6826
|
+
} else if (lt.operator === '<=' && !c.test(lt.semver)) {
|
|
6820
6827
|
return false
|
|
6821
6828
|
}
|
|
6822
6829
|
}
|
|
@@ -7112,12 +7119,26 @@ const DEFAULT_PORTS = {
|
|
|
7112
7119
|
* - `{configRoot}/jeeves-{name}/` for each component
|
|
7113
7120
|
*/
|
|
7114
7121
|
let state;
|
|
7122
|
+
const WINDOWS_DRIVE_RE = /^[a-zA-Z]:/;
|
|
7123
|
+
/**
|
|
7124
|
+
* Throw if a path looks like a Windows drive letter on a non-Windows platform.
|
|
7125
|
+
*
|
|
7126
|
+
* @param label - Human-readable name for the path (used in error messages).
|
|
7127
|
+
* @param value - The raw path string to validate.
|
|
7128
|
+
*/
|
|
7129
|
+
function rejectWindowsDrivePath(label, value) {
|
|
7130
|
+
if (process.platform !== 'win32' && WINDOWS_DRIVE_RE.test(value)) {
|
|
7131
|
+
throw new Error(`jeeves-core: ${label} "${value}" looks like a Windows drive-letter path and will not resolve correctly on this platform.`);
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7115
7134
|
/**
|
|
7116
7135
|
* Initialize the core library with workspace and config root paths.
|
|
7117
7136
|
*
|
|
7118
7137
|
* @param options - Workspace and config root paths.
|
|
7119
7138
|
*/
|
|
7120
7139
|
function init(options) {
|
|
7140
|
+
rejectWindowsDrivePath('configRoot', options.configRoot);
|
|
7141
|
+
rejectWindowsDrivePath('workspacePath', options.workspacePath);
|
|
7121
7142
|
state = {
|
|
7122
7143
|
workspacePath: options.workspacePath,
|
|
7123
7144
|
configRoot: options.configRoot,
|
|
@@ -8254,6 +8275,20 @@ When editing files outside the workspace, use the bridge pattern: copy in → ed
|
|
|
8254
8275
|
|
|
8255
8276
|
**Cross-channel sends:** Use the \`message\` tool with an explicit \`target\` to send to a different channel or DM.
|
|
8256
8277
|
|
|
8278
|
+
### Slack File Downloads
|
|
8279
|
+
|
|
8280
|
+
To download a Slack-hosted file, first try the \`message\` tool's \`download-file\` action. If that fails, fall back to a direct HTTP fetch using the bot token:
|
|
8281
|
+
|
|
8282
|
+
\`\`\`js
|
|
8283
|
+
fetch(url_private_download, {
|
|
8284
|
+
headers: { Authorization: 'Bearer ' + botToken },
|
|
8285
|
+
});
|
|
8286
|
+
\`\`\`
|
|
8287
|
+
|
|
8288
|
+
The bot token is at \`channels.slack.accounts.default.botToken\` in \`openclaw.json\`.
|
|
8289
|
+
|
|
8290
|
+
Never tell the user a file can't be downloaded until both methods have been tried.
|
|
8291
|
+
|
|
8257
8292
|
### Plugin Lifecycle
|
|
8258
8293
|
|
|
8259
8294
|
\`\`\`bash
|
|
@@ -8489,6 +8524,113 @@ const META_COMPONENT = {
|
|
|
8489
8524
|
pluginPackage: '@karmaniverous/jeeves-meta-openclaw',
|
|
8490
8525
|
defaultPort: 1938};
|
|
8491
8526
|
|
|
8527
|
+
/**
|
|
8528
|
+
* Shared endpoint catalog — single source of truth for the jeeves-meta API.
|
|
8529
|
+
*
|
|
8530
|
+
* Both the CLI service and the OpenClaw plugin derive their registrations
|
|
8531
|
+
* from this declarative catalog, eliminating drift between the two.
|
|
8532
|
+
*
|
|
8533
|
+
*/
|
|
8534
|
+
/**
|
|
8535
|
+
* Canonical endpoint catalog for the jeeves-meta API.
|
|
8536
|
+
*
|
|
8537
|
+
* Every entry describes a single HTTP endpoint exposed by the service.
|
|
8538
|
+
* Route handlers, plugin tools, and HTTP clients should reference these
|
|
8539
|
+
* descriptors rather than hard-coding paths and descriptions.
|
|
8540
|
+
*/
|
|
8541
|
+
const META_ENDPOINTS = [
|
|
8542
|
+
{
|
|
8543
|
+
name: 'status',
|
|
8544
|
+
method: 'GET',
|
|
8545
|
+
path: '/status',
|
|
8546
|
+
description: 'Service health and status overview.',
|
|
8547
|
+
},
|
|
8548
|
+
{
|
|
8549
|
+
name: 'listMetas',
|
|
8550
|
+
method: 'GET',
|
|
8551
|
+
path: '/metas',
|
|
8552
|
+
description: 'List metas with summary stats and per-meta projection. Response includes _phaseState and owedPhase per meta.',
|
|
8553
|
+
},
|
|
8554
|
+
{
|
|
8555
|
+
name: 'metaDetail',
|
|
8556
|
+
method: 'GET',
|
|
8557
|
+
path: '/metas/:path',
|
|
8558
|
+
description: 'Full detail for a single meta, with optional archive history. Response includes _phaseState and owedPhase.',
|
|
8559
|
+
},
|
|
8560
|
+
{
|
|
8561
|
+
name: 'updateMeta',
|
|
8562
|
+
method: 'PATCH',
|
|
8563
|
+
path: '/metas/:path',
|
|
8564
|
+
description: 'Update user-settable reserved properties on a meta entity.',
|
|
8565
|
+
},
|
|
8566
|
+
{
|
|
8567
|
+
name: 'synthesize',
|
|
8568
|
+
method: 'POST',
|
|
8569
|
+
path: '/synthesize',
|
|
8570
|
+
description: 'Trigger synthesis. Path-targeted creates an override queue entry; returns owedPhase. Fully-fresh metas return status:skipped.',
|
|
8571
|
+
},
|
|
8572
|
+
{
|
|
8573
|
+
name: 'abort',
|
|
8574
|
+
method: 'POST',
|
|
8575
|
+
path: '/synthesize/abort',
|
|
8576
|
+
description: 'Abort the currently running synthesis.',
|
|
8577
|
+
},
|
|
8578
|
+
{
|
|
8579
|
+
name: 'preview',
|
|
8580
|
+
method: 'GET',
|
|
8581
|
+
path: '/preview',
|
|
8582
|
+
description: 'Dry-run preview of next synthesis. Returns owedPhase, priorityBand, phaseState, inputStatus, and architectInvalidators.',
|
|
8583
|
+
},
|
|
8584
|
+
{
|
|
8585
|
+
name: 'seed',
|
|
8586
|
+
method: 'POST',
|
|
8587
|
+
path: '/seed',
|
|
8588
|
+
description: 'Create a .meta/ directory and initial meta.json for a new entity path.',
|
|
8589
|
+
},
|
|
8590
|
+
{
|
|
8591
|
+
name: 'unlock',
|
|
8592
|
+
method: 'POST',
|
|
8593
|
+
path: '/unlock',
|
|
8594
|
+
description: 'Remove a stale .lock from a meta entity that is stuck.',
|
|
8595
|
+
},
|
|
8596
|
+
{
|
|
8597
|
+
name: 'config',
|
|
8598
|
+
method: 'GET',
|
|
8599
|
+
path: '/config',
|
|
8600
|
+
description: 'Query service configuration with optional JSONPath.',
|
|
8601
|
+
},
|
|
8602
|
+
{
|
|
8603
|
+
name: 'configApply',
|
|
8604
|
+
method: 'POST',
|
|
8605
|
+
path: '/config/apply',
|
|
8606
|
+
description: 'Apply a configuration patch.',
|
|
8607
|
+
},
|
|
8608
|
+
{
|
|
8609
|
+
name: 'queue',
|
|
8610
|
+
method: 'GET',
|
|
8611
|
+
path: '/queue',
|
|
8612
|
+
description: 'List queued synthesis operations (3-layer model: current, overrides, automatic).',
|
|
8613
|
+
},
|
|
8614
|
+
{
|
|
8615
|
+
name: 'queueClear',
|
|
8616
|
+
method: 'POST',
|
|
8617
|
+
path: '/queue/clear',
|
|
8618
|
+
description: 'Clear override entries from the queue.',
|
|
8619
|
+
},
|
|
8620
|
+
];
|
|
8621
|
+
/**
|
|
8622
|
+
* Look up an endpoint descriptor by name.
|
|
8623
|
+
*
|
|
8624
|
+
* @param name - The endpoint identifier.
|
|
8625
|
+
* @returns The matching {@link EndpointDescriptor}.
|
|
8626
|
+
*/
|
|
8627
|
+
function getEndpoint(name) {
|
|
8628
|
+
const ep = META_ENDPOINTS.find((e) => e.name === name);
|
|
8629
|
+
if (!ep)
|
|
8630
|
+
throw new Error(`Unknown endpoint: ${name}`);
|
|
8631
|
+
return ep;
|
|
8632
|
+
}
|
|
8633
|
+
|
|
8492
8634
|
/**
|
|
8493
8635
|
* Structured error schema from a synthesis step failure.
|
|
8494
8636
|
*
|
|
@@ -8683,6 +8825,24 @@ async function discoverMetas(watcher) {
|
|
|
8683
8825
|
return metaPaths;
|
|
8684
8826
|
}
|
|
8685
8827
|
|
|
8828
|
+
/**
|
|
8829
|
+
* Retrieve the nearest ancestor meta node from the ownership tree.
|
|
8830
|
+
*
|
|
8831
|
+
* @module discovery/getAncestorMeta
|
|
8832
|
+
*/
|
|
8833
|
+
/**
|
|
8834
|
+
* Get the nearest ancestor MetaNode for a given node.
|
|
8835
|
+
*
|
|
8836
|
+
* Walks up the ownership tree (via the parent pointer set by
|
|
8837
|
+
* buildOwnershipTree) to find the closest ancestor .meta/ directory.
|
|
8838
|
+
*
|
|
8839
|
+
* @param node - The meta node to find the ancestor for.
|
|
8840
|
+
* @returns The parent MetaNode, or null for root-level metas.
|
|
8841
|
+
*/
|
|
8842
|
+
function getAncestorMeta(node) {
|
|
8843
|
+
return node.parent;
|
|
8844
|
+
}
|
|
8845
|
+
|
|
8686
8846
|
/**
|
|
8687
8847
|
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
8688
8848
|
*
|
|
@@ -8938,21 +9098,6 @@ async function isStale(scopePrefix, meta, watcher) {
|
|
|
8938
9098
|
}
|
|
8939
9099
|
/** Maximum staleness for never-synthesized metas (1 year in seconds). */
|
|
8940
9100
|
const MAX_STALENESS_SECONDS = 365 * 86_400;
|
|
8941
|
-
/**
|
|
8942
|
-
* Check whether the architect step should be triggered.
|
|
8943
|
-
*
|
|
8944
|
-
* @param meta - Current meta.json.
|
|
8945
|
-
* @param structureChanged - Whether the structure hash changed.
|
|
8946
|
-
* @param steerChanged - Whether the steer directive changed.
|
|
8947
|
-
* @param architectEvery - Config: run architect every N cycles.
|
|
8948
|
-
* @returns True if the architect step should run.
|
|
8949
|
-
*/
|
|
8950
|
-
function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
|
|
8951
|
-
return (!meta._builder ||
|
|
8952
|
-
structureChanged ||
|
|
8953
|
-
steerChanged ||
|
|
8954
|
-
(meta._synthesisCount ?? 0) >= architectEvery);
|
|
8955
|
-
}
|
|
8956
9101
|
/**
|
|
8957
9102
|
* Detect whether the steer directive changed since the last archive.
|
|
8958
9103
|
*
|
|
@@ -9339,6 +9484,14 @@ const autoSeedRuleSchema = z.object({
|
|
|
9339
9484
|
steer: z.string().optional(),
|
|
9340
9485
|
/** Optional cross-references for seeded metas. */
|
|
9341
9486
|
crossRefs: z.array(z.string()).optional(),
|
|
9487
|
+
/** Walk up this many extra parent levels from the matched file's directory. Default 0. */
|
|
9488
|
+
parentDepth: z.number().int().min(0).optional(),
|
|
9489
|
+
/** Per-category timeout override for the architect phase (seconds, min 30). */
|
|
9490
|
+
architectTimeout: z.number().int().min(30).optional(),
|
|
9491
|
+
/** Per-category timeout override for the builder phase (seconds, min 30). */
|
|
9492
|
+
builderTimeout: z.number().int().min(30).optional(),
|
|
9493
|
+
/** Per-category timeout override for the critic phase (seconds, min 30). */
|
|
9494
|
+
criticTimeout: z.number().int().min(30).optional(),
|
|
9342
9495
|
});
|
|
9343
9496
|
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
9344
9497
|
const serviceConfigSchema = metaConfigSchema.extend({
|
|
@@ -9356,6 +9509,8 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
9356
9509
|
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
9357
9510
|
/** Logging configuration. */
|
|
9358
9511
|
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
9512
|
+
/** Max number of all-fresh candidates to scan per tick in Tier 2 invalidation. */
|
|
9513
|
+
tier2ScanLimit: z.number().int().min(1).default(50),
|
|
9359
9514
|
/**
|
|
9360
9515
|
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
9361
9516
|
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
@@ -9583,7 +9738,7 @@ class GatewayExecutor {
|
|
|
9583
9738
|
'Write your complete output to a file using the Write tool at:\n' +
|
|
9584
9739
|
outputPath +
|
|
9585
9740
|
'\n\n' +
|
|
9586
|
-
'After writing the file,
|
|
9741
|
+
'After writing the file, your final message must be exactly: ANNOUNCE_SKIP';
|
|
9587
9742
|
// Step 1: Spawn the sub-agent session (unique label per cycle to avoid
|
|
9588
9743
|
// "label already in use" errors — gateway labels persist after session completion)
|
|
9589
9744
|
const labelBase = options?.label ?? 'jeeves-meta-synthesis';
|
|
@@ -9651,7 +9806,9 @@ class GatewayExecutor {
|
|
|
9651
9806
|
}
|
|
9652
9807
|
}
|
|
9653
9808
|
}
|
|
9654
|
-
// Fallback: extract from message content if file wasn't written
|
|
9809
|
+
// Fallback: extract from message content if file wasn't written.
|
|
9810
|
+
// Skip ANNOUNCE_SKIP sentinel messages — the real output is in
|
|
9811
|
+
// a preceding assistant message (the file write).
|
|
9655
9812
|
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9656
9813
|
const msg = msgArray[i];
|
|
9657
9814
|
if (msg.role === 'assistant' && msg.content) {
|
|
@@ -9663,7 +9820,7 @@ class GatewayExecutor {
|
|
|
9663
9820
|
.map((b) => b.text)
|
|
9664
9821
|
.join('\n')
|
|
9665
9822
|
: '';
|
|
9666
|
-
if (text)
|
|
9823
|
+
if (text && text.trim() !== 'ANNOUNCE_SKIP')
|
|
9667
9824
|
return { output: text, tokens };
|
|
9668
9825
|
}
|
|
9669
9826
|
}
|
|
@@ -9693,11 +9850,16 @@ class GatewayExecutor {
|
|
|
9693
9850
|
function createLogger(config) {
|
|
9694
9851
|
const level = config?.level ?? 'info';
|
|
9695
9852
|
if (config?.file) {
|
|
9696
|
-
const
|
|
9697
|
-
|
|
9698
|
-
|
|
9853
|
+
const fileStream = pino.destination({
|
|
9854
|
+
dest: config.file,
|
|
9855
|
+
sync: false,
|
|
9856
|
+
mkdir: true,
|
|
9699
9857
|
});
|
|
9700
|
-
|
|
9858
|
+
const multistream = pino.multistream([
|
|
9859
|
+
{ stream: process.stdout },
|
|
9860
|
+
{ stream: fileStream },
|
|
9861
|
+
]);
|
|
9862
|
+
return pino({ level }, multistream);
|
|
9701
9863
|
}
|
|
9702
9864
|
return pino({ level });
|
|
9703
9865
|
}
|
|
@@ -10007,6 +10169,21 @@ async function buildContextPackage(node, meta, watcher, logger) {
|
|
|
10007
10169
|
}
|
|
10008
10170
|
// Archive paths
|
|
10009
10171
|
const archives = listArchiveFiles(node.metaPath);
|
|
10172
|
+
// Nearest ancestor _builder output
|
|
10173
|
+
let ancestorBuilder;
|
|
10174
|
+
const ancestor = getAncestorMeta(node);
|
|
10175
|
+
if (ancestor) {
|
|
10176
|
+
try {
|
|
10177
|
+
const raw = await readFile(join(ancestor.metaPath, 'meta.json'), 'utf8');
|
|
10178
|
+
const ancestorMeta = JSON.parse(raw);
|
|
10179
|
+
if (ancestorMeta._builder) {
|
|
10180
|
+
ancestorBuilder = ancestorMeta._builder;
|
|
10181
|
+
}
|
|
10182
|
+
}
|
|
10183
|
+
catch {
|
|
10184
|
+
// Ancestor meta.json unreadable — skip
|
|
10185
|
+
}
|
|
10186
|
+
}
|
|
10010
10187
|
return {
|
|
10011
10188
|
path: node.metaPath,
|
|
10012
10189
|
scopeFiles,
|
|
@@ -10018,6 +10195,7 @@ async function buildContextPackage(node, meta, watcher, logger) {
|
|
|
10018
10195
|
steer: meta._steer ?? null,
|
|
10019
10196
|
previousState: meta._state ?? null,
|
|
10020
10197
|
archives,
|
|
10198
|
+
ancestorBuilder,
|
|
10021
10199
|
};
|
|
10022
10200
|
}
|
|
10023
10201
|
|
|
@@ -10065,6 +10243,12 @@ function appendMetaSections(sections, heading, metas) {
|
|
|
10065
10243
|
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
10066
10244
|
}
|
|
10067
10245
|
}
|
|
10246
|
+
/** Inject nearest ancestor's organizational context, if available. */
|
|
10247
|
+
function appendAncestorContext(sections, ctx) {
|
|
10248
|
+
if (ctx.ancestorBuilder) {
|
|
10249
|
+
sections.push('', '## PARENT ORGANIZATIONAL CONTEXT', ctx.ancestorBuilder);
|
|
10250
|
+
}
|
|
10251
|
+
}
|
|
10068
10252
|
/** Append optional context sections shared across all step prompts. */
|
|
10069
10253
|
function appendSharedSections(sections, ctx, options) {
|
|
10070
10254
|
const opts = {
|
|
@@ -10104,7 +10288,7 @@ function buildArchitectTask(ctx, meta, config) {
|
|
|
10104
10288
|
const sections = [
|
|
10105
10289
|
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
10106
10290
|
'',
|
|
10107
|
-
|
|
10291
|
+
config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
|
|
10108
10292
|
'',
|
|
10109
10293
|
'## SCOPE',
|
|
10110
10294
|
`Path: ${ctx.path}`,
|
|
@@ -10114,6 +10298,7 @@ function buildArchitectTask(ctx, meta, config) {
|
|
|
10114
10298
|
'### File listing (scope)',
|
|
10115
10299
|
condenseScopeFiles(ctx.scopeFiles),
|
|
10116
10300
|
];
|
|
10301
|
+
appendAncestorContext(sections, ctx);
|
|
10117
10302
|
// Inject previous _builder so architect can see its own prior output
|
|
10118
10303
|
if (meta._builder) {
|
|
10119
10304
|
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
@@ -10144,6 +10329,7 @@ function buildBuilderTask(ctx, meta, config) {
|
|
|
10144
10329
|
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
10145
10330
|
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
10146
10331
|
];
|
|
10332
|
+
appendAncestorContext(sections, ctx);
|
|
10147
10333
|
if (ctx.previousState != null) {
|
|
10148
10334
|
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), '```');
|
|
10149
10335
|
}
|
|
@@ -10166,7 +10352,7 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
10166
10352
|
const sections = [
|
|
10167
10353
|
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
10168
10354
|
'',
|
|
10169
|
-
|
|
10355
|
+
config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
10170
10356
|
'',
|
|
10171
10357
|
'## SYNTHESIS TO EVALUATE',
|
|
10172
10358
|
meta._content ?? '(No content produced)',
|
|
@@ -10300,7 +10486,7 @@ function enforceInvariant(state) {
|
|
|
10300
10486
|
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
10301
10487
|
/**
|
|
10302
10488
|
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
10303
|
-
* Triggers:
|
|
10489
|
+
* Triggers: first run, _structureHash change, _steer change,
|
|
10304
10490
|
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
10305
10491
|
*/
|
|
10306
10492
|
function invalidateArchitect(state) {
|
|
@@ -10498,31 +10684,6 @@ function derivePhaseState(meta, inputs) {
|
|
|
10498
10684
|
if (!meta._content && !meta._builder) {
|
|
10499
10685
|
return initialPhaseState();
|
|
10500
10686
|
}
|
|
10501
|
-
// Check architect invalidation (when inputs are provided)
|
|
10502
|
-
if (inputs) {
|
|
10503
|
-
// Progressive metas: structure changes invalidate builder, not architect
|
|
10504
|
-
const structureInvalidatesArchitect = inputs.structureChanged && meta._state === undefined;
|
|
10505
|
-
const architectInvalidated = structureInvalidatesArchitect ||
|
|
10506
|
-
inputs.steerChanged ||
|
|
10507
|
-
inputs.architectChanged ||
|
|
10508
|
-
inputs.crossRefsChanged ||
|
|
10509
|
-
(meta._synthesisCount ?? 0) >= inputs.architectEvery;
|
|
10510
|
-
if (architectInvalidated) {
|
|
10511
|
-
return {
|
|
10512
|
-
architect: 'pending',
|
|
10513
|
-
builder: 'stale',
|
|
10514
|
-
critic: 'stale',
|
|
10515
|
-
};
|
|
10516
|
-
}
|
|
10517
|
-
// Progressive meta with structure change: builder-only invalidation
|
|
10518
|
-
if (inputs.structureChanged && meta._state !== undefined) {
|
|
10519
|
-
return {
|
|
10520
|
-
architect: 'fresh',
|
|
10521
|
-
builder: 'pending',
|
|
10522
|
-
critic: 'stale',
|
|
10523
|
-
};
|
|
10524
|
-
}
|
|
10525
|
-
}
|
|
10526
10687
|
// Has _builder but no _content: builder is pending
|
|
10527
10688
|
if (meta._builder && !meta._content) {
|
|
10528
10689
|
return {
|
|
@@ -10571,6 +10732,14 @@ function computeStructureHash(filePaths) {
|
|
|
10571
10732
|
*
|
|
10572
10733
|
* @module phaseState/invalidate
|
|
10573
10734
|
*/
|
|
10735
|
+
/**
|
|
10736
|
+
* Check whether a persisted prompt snapshot mismatches the currently-resolved prompt.
|
|
10737
|
+
* Returns true when the snapshot exists and differs from the resolved prompt.
|
|
10738
|
+
* This is informational only — it does NOT trigger invalidation.
|
|
10739
|
+
*/
|
|
10740
|
+
function isPromptStale(snapshot, resolved) {
|
|
10741
|
+
return snapshot !== undefined && snapshot !== resolved;
|
|
10742
|
+
}
|
|
10574
10743
|
/**
|
|
10575
10744
|
* Compute invalidation inputs and apply cascade for a single meta.
|
|
10576
10745
|
*
|
|
@@ -10593,10 +10762,16 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
|
|
|
10593
10762
|
const structureChanged = structureHash !== meta._structureHash;
|
|
10594
10763
|
const latestArchive = await readLatestArchive(node.metaPath);
|
|
10595
10764
|
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10596
|
-
//
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10765
|
+
// Prompt staleness detection: compare persisted prompt snapshots against
|
|
10766
|
+
// currently-resolved prompts. This is INFORMATIONAL ONLY — reported via
|
|
10767
|
+
// inputStatus so /preview can surface it, but it must NEVER feed into
|
|
10768
|
+
// the invalidation cascade. When a meta naturally reaches architectEvery
|
|
10769
|
+
// through real builder cycles, architect runs with the current prompt and
|
|
10770
|
+
// the snapshot updates. Coupling prompt changes to invalidation causes a
|
|
10771
|
+
// corpus-wide synthesis storm (see #163).
|
|
10772
|
+
const architectChanged = isPromptStale(meta._architect, config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT);
|
|
10773
|
+
const criticChanged = isPromptStale(meta._critic, config.defaultCritic ?? DEFAULT_CRITIC_PROMPT);
|
|
10774
|
+
const effectiveSynthesisCount = meta._synthesisCount ?? 0;
|
|
10600
10775
|
// _crossRefs declaration change
|
|
10601
10776
|
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
10602
10777
|
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
@@ -10618,38 +10793,30 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
|
|
|
10618
10793
|
}
|
|
10619
10794
|
if (steerChanged)
|
|
10620
10795
|
architectInvalidators.push('steer');
|
|
10621
|
-
if (architectChanged)
|
|
10622
|
-
architectInvalidators.push('_architect');
|
|
10623
10796
|
if (crossRefsDeclChanged)
|
|
10624
10797
|
architectInvalidators.push('_crossRefs');
|
|
10625
|
-
if (
|
|
10798
|
+
if (effectiveSynthesisCount >= config.architectEvery) {
|
|
10626
10799
|
architectInvalidators.push('architectEvery');
|
|
10627
10800
|
}
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
if (architectInvalidators.length > 0
|
|
10801
|
+
if (!meta._builder)
|
|
10802
|
+
architectInvalidators.push('firstRun');
|
|
10803
|
+
if (architectInvalidators.length > 0) {
|
|
10631
10804
|
phaseState = invalidateArchitect(phaseState);
|
|
10632
10805
|
}
|
|
10633
10806
|
// ── Builder-level inputs ──
|
|
10634
|
-
// Scope file mtime check — if any file newer than _generatedAt
|
|
10635
|
-
const scopeMtimeMax = null;
|
|
10636
|
-
// Note: actual mtime check is done by the caller or via isStale;
|
|
10637
|
-
// here we just detect cross-ref content changes for the cascade.
|
|
10638
10807
|
// Cross-ref _content change (builder-invalidating)
|
|
10639
10808
|
let crossRefContentChanged = false;
|
|
10640
10809
|
return {
|
|
10641
10810
|
phaseState,
|
|
10642
10811
|
architectInvalidators,
|
|
10643
|
-
|
|
10812
|
+
inputStatus: {
|
|
10644
10813
|
structureHash,
|
|
10645
10814
|
steerChanged,
|
|
10646
10815
|
architectChanged,
|
|
10816
|
+
criticChanged,
|
|
10647
10817
|
crossRefsDeclChanged,
|
|
10648
|
-
scopeMtimeMax,
|
|
10649
10818
|
crossRefContentChanged,
|
|
10650
10819
|
},
|
|
10651
|
-
structureHash,
|
|
10652
|
-
steerChanged,
|
|
10653
10820
|
};
|
|
10654
10821
|
}
|
|
10655
10822
|
|
|
@@ -10789,20 +10956,14 @@ function selectPhaseCandidate(metas, depthWeight) {
|
|
|
10789
10956
|
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
10790
10957
|
}
|
|
10791
10958
|
/**
|
|
10792
|
-
* Select
|
|
10793
|
-
*
|
|
10794
|
-
* have structural or steer changes detectable only via I/O.
|
|
10795
|
-
*
|
|
10796
|
-
* @param metas - Phase candidate inputs (after Tier 1 filtering).
|
|
10797
|
-
* @returns The stalest all-fresh candidate, or null if none exist.
|
|
10959
|
+
* Select all fully-fresh, non-disabled, non-locked metas sorted by staleness
|
|
10960
|
+
* (descending — stalest first) for Tier 2 invalidation scanning.
|
|
10798
10961
|
*/
|
|
10799
|
-
function
|
|
10800
|
-
|
|
10962
|
+
function selectAllTier2Candidates(metas) {
|
|
10963
|
+
return metas
|
|
10801
10964
|
.filter((m) => !m.locked && !m.disabled && isFullyFresh(m.phaseState))
|
|
10802
|
-
.sort((a, b) => b.actualStaleness - a.actualStaleness)
|
|
10803
|
-
|
|
10804
|
-
return null;
|
|
10805
|
-
return { node: eligible[0].node, meta: eligible[0].meta };
|
|
10965
|
+
.sort((a, b) => b.actualStaleness - a.actualStaleness)
|
|
10966
|
+
.map((m) => ({ node: m.node, meta: m.meta }));
|
|
10806
10967
|
}
|
|
10807
10968
|
|
|
10808
10969
|
/**
|
|
@@ -10835,6 +10996,15 @@ function toMetaError(step, err, code = 'FAILED') {
|
|
|
10835
10996
|
*
|
|
10836
10997
|
* @module orchestrator/parseOutput
|
|
10837
10998
|
*/
|
|
10999
|
+
/** Sentinel appended by synthesis workers to skip the announce turn. */
|
|
11000
|
+
const ANNOUNCE_SKIP = 'ANNOUNCE_SKIP';
|
|
11001
|
+
/** Strip a trailing ANNOUNCE_SKIP sentinel from raw output. */
|
|
11002
|
+
function stripSentinel(raw) {
|
|
11003
|
+
const trimmed = raw.trim();
|
|
11004
|
+
return trimmed.endsWith(ANNOUNCE_SKIP)
|
|
11005
|
+
? trimmed.slice(0, -ANNOUNCE_SKIP.length).trim()
|
|
11006
|
+
: trimmed;
|
|
11007
|
+
}
|
|
10838
11008
|
/**
|
|
10839
11009
|
* Parse architect output. The architect returns a task brief as text.
|
|
10840
11010
|
*
|
|
@@ -10842,7 +11012,7 @@ function toMetaError(step, err, code = 'FAILED') {
|
|
|
10842
11012
|
* @returns The task brief string.
|
|
10843
11013
|
*/
|
|
10844
11014
|
function parseArchitectOutput(output) {
|
|
10845
|
-
return output
|
|
11015
|
+
return stripSentinel(output);
|
|
10846
11016
|
}
|
|
10847
11017
|
/**
|
|
10848
11018
|
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
@@ -10853,7 +11023,7 @@ function parseArchitectOutput(output) {
|
|
|
10853
11023
|
* @returns Parsed builder output with content and structured fields.
|
|
10854
11024
|
*/
|
|
10855
11025
|
function parseBuilderOutput(output) {
|
|
10856
|
-
const trimmed = output
|
|
11026
|
+
const trimmed = stripSentinel(output);
|
|
10857
11027
|
// Strategy 1: Try to parse the entire output as JSON directly
|
|
10858
11028
|
const direct = tryParseJson(trimmed);
|
|
10859
11029
|
if (direct)
|
|
@@ -10920,7 +11090,7 @@ function tryParseJson(str) {
|
|
|
10920
11090
|
* @returns The feedback string.
|
|
10921
11091
|
*/
|
|
10922
11092
|
function parseCriticOutput(output) {
|
|
10923
|
-
return output
|
|
11093
|
+
return stripSentinel(output);
|
|
10924
11094
|
}
|
|
10925
11095
|
|
|
10926
11096
|
/**
|
|
@@ -10931,6 +11101,12 @@ function parseCriticOutput(output) {
|
|
|
10931
11101
|
*
|
|
10932
11102
|
* @module orchestrator/runPhase
|
|
10933
11103
|
*/
|
|
11104
|
+
/** Compute SHA-256 hash of ancestor _builder text for observability tracking. */
|
|
11105
|
+
function hashAncestorBuilder(ancestorBuilder) {
|
|
11106
|
+
return ancestorBuilder
|
|
11107
|
+
? createHash('sha256').update(ancestorBuilder).digest('hex')
|
|
11108
|
+
: undefined;
|
|
11109
|
+
}
|
|
10934
11110
|
/** Write updated meta with phase state via lock staging. */
|
|
10935
11111
|
async function persistPhaseState(base, phaseState, updates) {
|
|
10936
11112
|
const lockPath = join(base.metaPath, '.lock');
|
|
@@ -10974,6 +11150,12 @@ async function handlePhaseFailure(phase, err, executor, ps, base, additionalUpda
|
|
|
10974
11150
|
// ── Architect executor ─────────────────────────────────────────────────
|
|
10975
11151
|
async function runArchitect(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10976
11152
|
let ps = phaseRunning(phaseState, 'architect');
|
|
11153
|
+
const base = {
|
|
11154
|
+
metaPath: node.metaPath,
|
|
11155
|
+
current: currentMeta,
|
|
11156
|
+
config,
|
|
11157
|
+
structureHash,
|
|
11158
|
+
};
|
|
10977
11159
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10978
11160
|
try {
|
|
10979
11161
|
await onProgress?.({
|
|
@@ -10985,21 +11167,25 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
10985
11167
|
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
10986
11168
|
const result = await executor.spawn(architectTask, {
|
|
10987
11169
|
thinking: config.thinking,
|
|
10988
|
-
timeout: config.architectTimeout,
|
|
11170
|
+
timeout: currentMeta._architectTimeout ?? config.architectTimeout,
|
|
10989
11171
|
label: 'meta-architect',
|
|
10990
11172
|
});
|
|
10991
11173
|
const builderBrief = parseArchitectOutput(result.output);
|
|
10992
11174
|
const architectTokens = result.tokens;
|
|
10993
11175
|
// Architect success: architect → fresh, _synthesisCount → 0
|
|
10994
11176
|
ps = architectSuccess(ps);
|
|
10995
|
-
const
|
|
11177
|
+
const architectUpdates = {
|
|
10996
11178
|
_builder: builderBrief,
|
|
10997
|
-
_architect:
|
|
11179
|
+
_architect: config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
|
|
10998
11180
|
_synthesisCount: 0,
|
|
10999
11181
|
_architectTokens: architectTokens,
|
|
11000
11182
|
_generatedAt: new Date().toISOString(),
|
|
11001
11183
|
_error: undefined,
|
|
11002
|
-
}
|
|
11184
|
+
};
|
|
11185
|
+
const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
|
|
11186
|
+
if (ancestorHash)
|
|
11187
|
+
architectUpdates._ancestorBuilderHash = ancestorHash;
|
|
11188
|
+
const updatedMeta = await persistPhaseState(base, ps, architectUpdates);
|
|
11003
11189
|
await onProgress?.({
|
|
11004
11190
|
type: 'phase_complete',
|
|
11005
11191
|
path: node.ownerPath,
|
|
@@ -11010,16 +11196,18 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
11010
11196
|
return { executed: true, phaseState: ps, updatedMeta };
|
|
11011
11197
|
}
|
|
11012
11198
|
catch (err) {
|
|
11013
|
-
return handlePhaseFailure('architect', err, executor, ps,
|
|
11014
|
-
metaPath: node.metaPath,
|
|
11015
|
-
current: currentMeta,
|
|
11016
|
-
structureHash,
|
|
11017
|
-
});
|
|
11199
|
+
return handlePhaseFailure('architect', err, executor, ps, base);
|
|
11018
11200
|
}
|
|
11019
11201
|
}
|
|
11020
11202
|
// ── Builder executor ───────────────────────────────────────────────────
|
|
11021
11203
|
async function runBuilder(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
11022
11204
|
let ps = phaseRunning(phaseState, 'builder');
|
|
11205
|
+
const base = {
|
|
11206
|
+
metaPath: node.metaPath,
|
|
11207
|
+
current: currentMeta,
|
|
11208
|
+
config,
|
|
11209
|
+
structureHash,
|
|
11210
|
+
};
|
|
11023
11211
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
11024
11212
|
try {
|
|
11025
11213
|
await onProgress?.({
|
|
@@ -11031,21 +11219,25 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
11031
11219
|
const builderTask = buildBuilderTask(ctx, currentMeta, config);
|
|
11032
11220
|
const result = await executor.spawn(builderTask, {
|
|
11033
11221
|
thinking: config.thinking,
|
|
11034
|
-
timeout: config.builderTimeout,
|
|
11222
|
+
timeout: currentMeta._builderTimeout ?? config.builderTimeout,
|
|
11035
11223
|
label: 'meta-builder',
|
|
11036
11224
|
});
|
|
11037
11225
|
const builderOutput = parseBuilderOutput(result.output);
|
|
11038
11226
|
const builderTokens = result.tokens;
|
|
11039
11227
|
// Builder success: builder → fresh, critic → pending
|
|
11040
11228
|
ps = builderSuccess(ps);
|
|
11041
|
-
const
|
|
11229
|
+
const builderUpdates = {
|
|
11042
11230
|
_content: builderOutput.content,
|
|
11043
11231
|
_state: builderOutput.state,
|
|
11044
11232
|
_builderTokens: builderTokens,
|
|
11045
11233
|
_generatedAt: new Date().toISOString(),
|
|
11046
11234
|
_error: undefined,
|
|
11047
11235
|
...builderOutput.fields,
|
|
11048
|
-
}
|
|
11236
|
+
};
|
|
11237
|
+
const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
|
|
11238
|
+
if (ancestorHash)
|
|
11239
|
+
builderUpdates._ancestorBuilderHash = ancestorHash;
|
|
11240
|
+
const updatedMeta = await persistPhaseState(base, ps, builderUpdates);
|
|
11049
11241
|
await onProgress?.({
|
|
11050
11242
|
type: 'phase_complete',
|
|
11051
11243
|
path: node.ownerPath,
|
|
@@ -11071,16 +11263,18 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
11071
11263
|
// Could not read partial output — no state recovery
|
|
11072
11264
|
}
|
|
11073
11265
|
}
|
|
11074
|
-
return handlePhaseFailure('builder', err, executor, ps,
|
|
11075
|
-
metaPath: node.metaPath,
|
|
11076
|
-
current: currentMeta,
|
|
11077
|
-
structureHash,
|
|
11078
|
-
}, partialState);
|
|
11266
|
+
return handlePhaseFailure('builder', err, executor, ps, base, partialState);
|
|
11079
11267
|
}
|
|
11080
11268
|
}
|
|
11081
11269
|
// ── Critic executor ────────────────────────────────────────────────────
|
|
11082
11270
|
async function runCritic(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
11083
11271
|
let ps = phaseRunning(phaseState, 'critic');
|
|
11272
|
+
const base = {
|
|
11273
|
+
metaPath: node.metaPath,
|
|
11274
|
+
current: currentMeta,
|
|
11275
|
+
config,
|
|
11276
|
+
structureHash,
|
|
11277
|
+
};
|
|
11084
11278
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
11085
11279
|
// Build critic task using current meta's _content
|
|
11086
11280
|
const metaForCritic = { ...currentMeta };
|
|
@@ -11094,7 +11288,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
11094
11288
|
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
11095
11289
|
const result = await executor.spawn(criticTask, {
|
|
11096
11290
|
thinking: config.thinking,
|
|
11097
|
-
timeout: config.criticTimeout,
|
|
11291
|
+
timeout: currentMeta._criticTimeout ?? config.criticTimeout,
|
|
11098
11292
|
label: 'meta-critic',
|
|
11099
11293
|
});
|
|
11100
11294
|
const feedback = parseCriticOutput(result.output);
|
|
@@ -11104,6 +11298,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
11104
11298
|
const cycleComplete = isFullyFresh(ps);
|
|
11105
11299
|
const updates = {
|
|
11106
11300
|
_feedback: feedback,
|
|
11301
|
+
_critic: config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
11107
11302
|
_criticTokens: criticTokens,
|
|
11108
11303
|
_error: undefined,
|
|
11109
11304
|
};
|
|
@@ -11112,7 +11307,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
11112
11307
|
if (cycleComplete) {
|
|
11113
11308
|
updates._synthesisCount = (currentMeta._synthesisCount ?? 0) + 1;
|
|
11114
11309
|
}
|
|
11115
|
-
const updatedMeta = await persistPhaseState(
|
|
11310
|
+
const updatedMeta = await persistPhaseState(base, ps, updates);
|
|
11116
11311
|
// Archive on full-cycle only
|
|
11117
11312
|
if (cycleComplete) {
|
|
11118
11313
|
await createSnapshot(node.metaPath, updatedMeta);
|
|
@@ -11133,11 +11328,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
11133
11328
|
};
|
|
11134
11329
|
}
|
|
11135
11330
|
catch (err) {
|
|
11136
|
-
return handlePhaseFailure('critic', err, executor, ps,
|
|
11137
|
-
metaPath: node.metaPath,
|
|
11138
|
-
current: currentMeta,
|
|
11139
|
-
structureHash,
|
|
11140
|
-
});
|
|
11331
|
+
return handlePhaseFailure('critic', err, executor, ps, base);
|
|
11141
11332
|
}
|
|
11142
11333
|
}
|
|
11143
11334
|
|
|
@@ -11185,8 +11376,9 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
|
|
|
11185
11376
|
// Select best phase candidate
|
|
11186
11377
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
11187
11378
|
if (!winner) {
|
|
11188
|
-
//
|
|
11189
|
-
|
|
11379
|
+
// Tier 2 is now handled by the scheduler; orchestratePhase only handles
|
|
11380
|
+
// targeted (override) paths and Tier 1 corpus-wide selection.
|
|
11381
|
+
return { executed: false };
|
|
11190
11382
|
}
|
|
11191
11383
|
// Acquire lock
|
|
11192
11384
|
if (!acquireLock(winner.node.metaPath)) {
|
|
@@ -11252,48 +11444,6 @@ async function orchestrateTargeted(config, executor, watcher, targetPath, onProg
|
|
|
11252
11444
|
releaseLock(node.metaPath);
|
|
11253
11445
|
}
|
|
11254
11446
|
}
|
|
11255
|
-
/**
|
|
11256
|
-
* Tier 2 invalidation fallback: pick the stalest all-fresh meta,
|
|
11257
|
-
* run computeInvalidation (structure hash, steer, cross-refs), and
|
|
11258
|
-
* either execute the owed phase or bump _generatedAt.
|
|
11259
|
-
*/
|
|
11260
|
-
async function orchestrateTier2(candidates, config, executor, watcher, onProgress, logger) {
|
|
11261
|
-
const tier2 = selectTier2Candidate(candidates);
|
|
11262
|
-
if (!tier2)
|
|
11263
|
-
return { executed: false };
|
|
11264
|
-
if (!acquireLock(tier2.node.metaPath)) {
|
|
11265
|
-
logger?.debug({ path: tier2.node.metaPath }, 'Tier 2 candidate is locked, skipping');
|
|
11266
|
-
return { executed: false };
|
|
11267
|
-
}
|
|
11268
|
-
try {
|
|
11269
|
-
const currentMeta = await readMetaJson(tier2.node.metaPath);
|
|
11270
|
-
const { scopeFiles } = await getScopeFiles(tier2.node, watcher);
|
|
11271
|
-
const { phaseState, structureHash } = await computeInvalidation(currentMeta, scopeFiles, config, tier2.node);
|
|
11272
|
-
const owedPhase = getOwedPhase(phaseState);
|
|
11273
|
-
if (owedPhase) {
|
|
11274
|
-
// Something changed — persist invalidated state and execute owed phase
|
|
11275
|
-
await persistPhaseState({
|
|
11276
|
-
metaPath: tier2.node.metaPath,
|
|
11277
|
-
current: currentMeta,
|
|
11278
|
-
config,
|
|
11279
|
-
structureHash,
|
|
11280
|
-
}, phaseState, {});
|
|
11281
|
-
return await executePhase(tier2.node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
|
|
11282
|
-
}
|
|
11283
|
-
// Nothing changed — bump _generatedAt to delay re-checking
|
|
11284
|
-
await persistPhaseState({
|
|
11285
|
-
metaPath: tier2.node.metaPath,
|
|
11286
|
-
current: currentMeta,
|
|
11287
|
-
config,
|
|
11288
|
-
structureHash,
|
|
11289
|
-
}, phaseState, { _generatedAt: new Date().toISOString() });
|
|
11290
|
-
logger?.debug({ path: tier2.node.ownerPath }, 'Tier 2: no invalidation detected, bumped _generatedAt');
|
|
11291
|
-
return { executed: false };
|
|
11292
|
-
}
|
|
11293
|
-
finally {
|
|
11294
|
-
releaseLock(tier2.node.metaPath);
|
|
11295
|
-
}
|
|
11296
|
-
}
|
|
11297
11447
|
/**
|
|
11298
11448
|
* Execute exactly one phase on a meta.
|
|
11299
11449
|
*/
|
|
@@ -11755,12 +11905,6 @@ class WatcherHealthCheck {
|
|
|
11755
11905
|
return;
|
|
11756
11906
|
}
|
|
11757
11907
|
const data = (await res.json());
|
|
11758
|
-
// If rules were never successfully registered (startup failure),
|
|
11759
|
-
// attempt registration now that the watcher is reachable.
|
|
11760
|
-
if (!this.registrar.isRegistered) {
|
|
11761
|
-
this.logger.info('Rules not registered — attempting registration');
|
|
11762
|
-
await this.registrar.register();
|
|
11763
|
-
}
|
|
11764
11908
|
await this.registrar.checkAndReregister(data.uptime);
|
|
11765
11909
|
}
|
|
11766
11910
|
catch (err) {
|
|
@@ -11913,36 +12057,39 @@ class RuleRegistrar {
|
|
|
11913
12057
|
logger;
|
|
11914
12058
|
watcherClient;
|
|
11915
12059
|
lastWatcherUptime = null;
|
|
11916
|
-
|
|
12060
|
+
registering = false;
|
|
11917
12061
|
constructor(config, logger, watcher) {
|
|
11918
12062
|
this.config = config;
|
|
11919
12063
|
this.logger = logger;
|
|
11920
12064
|
this.watcherClient = watcher;
|
|
11921
12065
|
}
|
|
11922
|
-
/** Whether rules have been successfully registered. */
|
|
11923
|
-
get isRegistered() {
|
|
11924
|
-
return this.registered;
|
|
11925
|
-
}
|
|
11926
12066
|
/**
|
|
11927
12067
|
* Register rules with watcher. Retries with exponential backoff.
|
|
11928
12068
|
* Non-blocking — logs errors but never throws.
|
|
11929
12069
|
*/
|
|
11930
12070
|
async register() {
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
12071
|
+
if (this.registering)
|
|
12072
|
+
return;
|
|
12073
|
+
this.registering = true;
|
|
12074
|
+
try {
|
|
12075
|
+
const rules = buildMetaRules(this.config);
|
|
12076
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
12077
|
+
try {
|
|
12078
|
+
await this.watcherClient.registerRules(SOURCE, rules);
|
|
12079
|
+
this.logger.info('Virtual rules registered with watcher');
|
|
12080
|
+
return;
|
|
12081
|
+
}
|
|
12082
|
+
catch (err) {
|
|
12083
|
+
const delayMs = RETRY_BASE_MS * Math.pow(2, attempt);
|
|
12084
|
+
this.logger.warn({ attempt: attempt + 1, delayMs, err }, 'Rule registration failed, retrying');
|
|
12085
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
12086
|
+
}
|
|
11943
12087
|
}
|
|
12088
|
+
this.logger.error('Rule registration failed after max retries — service degraded');
|
|
12089
|
+
}
|
|
12090
|
+
finally {
|
|
12091
|
+
this.registering = false;
|
|
11944
12092
|
}
|
|
11945
|
-
this.logger.error('Rule registration failed after max retries — service degraded');
|
|
11946
12093
|
}
|
|
11947
12094
|
/**
|
|
11948
12095
|
* Check watcher uptime and re-register if it decreased (restart detected).
|
|
@@ -11953,7 +12100,6 @@ class RuleRegistrar {
|
|
|
11953
12100
|
if (this.lastWatcherUptime !== null &&
|
|
11954
12101
|
currentUptime < this.lastWatcherUptime) {
|
|
11955
12102
|
this.logger.info({ previous: this.lastWatcherUptime, current: currentUptime }, 'Watcher restart detected — re-registering rules');
|
|
11956
|
-
this.registered = false;
|
|
11957
12103
|
await this.register();
|
|
11958
12104
|
}
|
|
11959
12105
|
this.lastWatcherUptime = currentUptime;
|
|
@@ -12023,6 +12169,12 @@ async function createMeta(ownerPath, options) {
|
|
|
12023
12169
|
metaJson._crossRefs = options.crossRefs;
|
|
12024
12170
|
if (options?.steer !== undefined)
|
|
12025
12171
|
metaJson._steer = options.steer;
|
|
12172
|
+
if (options?.architectTimeout !== undefined)
|
|
12173
|
+
metaJson._architectTimeout = options.architectTimeout;
|
|
12174
|
+
if (options?.builderTimeout !== undefined)
|
|
12175
|
+
metaJson._builderTimeout = options.builderTimeout;
|
|
12176
|
+
if (options?.criticTimeout !== undefined)
|
|
12177
|
+
metaJson._criticTimeout = options.criticTimeout;
|
|
12026
12178
|
const metaJsonPath = join(metaDir, 'meta.json');
|
|
12027
12179
|
await writeFile(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
12028
12180
|
return { metaDir, _id };
|
|
@@ -12051,15 +12203,24 @@ function metaExists(ownerPath) {
|
|
|
12051
12203
|
/**
|
|
12052
12204
|
* Extract parent directory paths from watcher walk results.
|
|
12053
12205
|
*
|
|
12054
|
-
* Walk returns file paths; we need the unique set of
|
|
12055
|
-
*
|
|
12206
|
+
* Walk returns file paths; we need the unique set of parent directories that
|
|
12207
|
+
* could be owners. When {@link parentDepth} is specified, walk up that many
|
|
12208
|
+
* additional levels from each file's immediate parent. The walk is clamped at
|
|
12209
|
+
* the filesystem root to prevent escaping the watched scope.
|
|
12056
12210
|
*/
|
|
12057
|
-
function extractDirectories(filePaths, logger) {
|
|
12211
|
+
function extractDirectories(filePaths, parentDepth = 0, logger) {
|
|
12058
12212
|
const dirs = new Set();
|
|
12059
12213
|
for (const fp of filePaths) {
|
|
12060
12214
|
// Normalize backslash paths (Windows) to forward slashes before posix.dirname
|
|
12061
12215
|
const normalized = normalizePath(fp);
|
|
12062
|
-
|
|
12216
|
+
let dir = posix.dirname(normalized);
|
|
12217
|
+
// Walk up parentDepth additional levels, clamping at filesystem root
|
|
12218
|
+
for (let i = 0; i < parentDepth; i++) {
|
|
12219
|
+
const parent = posix.dirname(dir);
|
|
12220
|
+
if (parent === dir)
|
|
12221
|
+
break; // reached root
|
|
12222
|
+
dir = parent;
|
|
12223
|
+
}
|
|
12063
12224
|
if (dir !== '.' && dir !== '/') {
|
|
12064
12225
|
dirs.add(dir);
|
|
12065
12226
|
}
|
|
@@ -12084,11 +12245,14 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
12084
12245
|
const candidates = new Map();
|
|
12085
12246
|
for (const rule of rules) {
|
|
12086
12247
|
const files = await watcher.walk([rule.match]);
|
|
12087
|
-
const dirs = extractDirectories(files, logger);
|
|
12248
|
+
const dirs = extractDirectories(files, rule.parentDepth, logger);
|
|
12088
12249
|
for (const dir of dirs) {
|
|
12089
12250
|
candidates.set(dir, {
|
|
12090
12251
|
steer: rule.steer,
|
|
12091
12252
|
crossRefs: rule.crossRefs,
|
|
12253
|
+
architectTimeout: rule.architectTimeout,
|
|
12254
|
+
builderTimeout: rule.builderTimeout,
|
|
12255
|
+
criticTimeout: rule.criticTimeout,
|
|
12092
12256
|
});
|
|
12093
12257
|
}
|
|
12094
12258
|
}
|
|
@@ -12103,10 +12267,7 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
12103
12267
|
const seededPaths = [];
|
|
12104
12268
|
for (const candidate of toSeed) {
|
|
12105
12269
|
try {
|
|
12106
|
-
await createMeta(candidate.path,
|
|
12107
|
-
steer: candidate.steer,
|
|
12108
|
-
crossRefs: candidate.crossRefs,
|
|
12109
|
-
});
|
|
12270
|
+
await createMeta(candidate.path, candidate);
|
|
12110
12271
|
seededPaths.push(candidate.path);
|
|
12111
12272
|
logger?.info({ path: candidate.path }, 'auto-seeded meta');
|
|
12112
12273
|
}
|
|
@@ -12271,7 +12432,7 @@ class Scheduler {
|
|
|
12271
12432
|
const candidates = buildPhaseCandidates(result.entries, this.config.architectEvery);
|
|
12272
12433
|
const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
|
|
12273
12434
|
if (!winner)
|
|
12274
|
-
return
|
|
12435
|
+
return await this.discoverTier2Phase(candidates);
|
|
12275
12436
|
return {
|
|
12276
12437
|
path: winner.node.metaPath,
|
|
12277
12438
|
phase: winner.owedPhase,
|
|
@@ -12283,6 +12444,59 @@ class Scheduler {
|
|
|
12283
12444
|
return null;
|
|
12284
12445
|
}
|
|
12285
12446
|
}
|
|
12447
|
+
/**
|
|
12448
|
+
* Tier 2 invalidation: iterate all-fresh candidates (stalest first),
|
|
12449
|
+
* run computeInvalidation, and return the first that produces an owed phase.
|
|
12450
|
+
*/
|
|
12451
|
+
async discoverTier2Phase(candidates) {
|
|
12452
|
+
const allTier2 = selectAllTier2Candidates(candidates);
|
|
12453
|
+
const limit = this.config.tier2ScanLimit;
|
|
12454
|
+
const tier2Candidates = allTier2.slice(0, limit);
|
|
12455
|
+
if (allTier2.length > limit) {
|
|
12456
|
+
this.logger.debug({ total: allTier2.length, limit }, 'Tier 2 scan limit reached, scanning subset');
|
|
12457
|
+
}
|
|
12458
|
+
let dirty = false;
|
|
12459
|
+
for (const t2 of tier2Candidates) {
|
|
12460
|
+
if (!acquireLock(t2.node.metaPath))
|
|
12461
|
+
continue;
|
|
12462
|
+
try {
|
|
12463
|
+
const currentMeta = await readMetaJson(t2.node.metaPath);
|
|
12464
|
+
const { scopeFiles } = await getScopeFiles(t2.node, this.watcher);
|
|
12465
|
+
const result = await computeInvalidation(currentMeta, scopeFiles, this.config, t2.node);
|
|
12466
|
+
const owedPhase = getOwedPhase(result.phaseState);
|
|
12467
|
+
if (owedPhase) {
|
|
12468
|
+
await persistPhaseState({
|
|
12469
|
+
metaPath: t2.node.metaPath,
|
|
12470
|
+
current: currentMeta,
|
|
12471
|
+
config: this.config,
|
|
12472
|
+
structureHash: result.inputStatus.structureHash,
|
|
12473
|
+
}, result.phaseState, {});
|
|
12474
|
+
this.cache.invalidate();
|
|
12475
|
+
return {
|
|
12476
|
+
path: t2.node.metaPath,
|
|
12477
|
+
phase: owedPhase,
|
|
12478
|
+
band: getPriorityBand(result.phaseState),
|
|
12479
|
+
};
|
|
12480
|
+
}
|
|
12481
|
+
// No invalidation — bump _generatedAt to delay re-checking
|
|
12482
|
+
await persistPhaseState({
|
|
12483
|
+
metaPath: t2.node.metaPath,
|
|
12484
|
+
current: currentMeta,
|
|
12485
|
+
config: this.config,
|
|
12486
|
+
structureHash: result.inputStatus.structureHash,
|
|
12487
|
+
}, result.phaseState, {
|
|
12488
|
+
_generatedAt: new Date().toISOString(),
|
|
12489
|
+
});
|
|
12490
|
+
dirty = true;
|
|
12491
|
+
}
|
|
12492
|
+
finally {
|
|
12493
|
+
releaseLock(t2.node.metaPath);
|
|
12494
|
+
}
|
|
12495
|
+
}
|
|
12496
|
+
if (dirty)
|
|
12497
|
+
this.cache.invalidate();
|
|
12498
|
+
return null;
|
|
12499
|
+
}
|
|
12286
12500
|
}
|
|
12287
12501
|
|
|
12288
12502
|
/**
|
|
@@ -12302,7 +12516,7 @@ function sanitizeConfig(config) {
|
|
|
12302
12516
|
}
|
|
12303
12517
|
function registerConfigRoute(app, deps) {
|
|
12304
12518
|
const configHandler = createConfigQueryHandler(() => sanitizeConfig(deps.config));
|
|
12305
|
-
app.get('
|
|
12519
|
+
app.get(getEndpoint('config').path, async (request, reply) => {
|
|
12306
12520
|
const { path } = request.query;
|
|
12307
12521
|
const result = await configHandler({ path });
|
|
12308
12522
|
return reply.status(result.status).send(result.body);
|
|
@@ -12321,7 +12535,7 @@ function registerConfigRoute(app, deps) {
|
|
|
12321
12535
|
*/
|
|
12322
12536
|
/** Register the POST /config/apply route. */
|
|
12323
12537
|
function registerConfigApplyRoute(app, configPath) {
|
|
12324
|
-
app.post('
|
|
12538
|
+
app.post(getEndpoint('configApply').path, async (request, reply) => {
|
|
12325
12539
|
if (!configPath) {
|
|
12326
12540
|
return reply
|
|
12327
12541
|
.status(500)
|
|
@@ -12410,42 +12624,32 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
12410
12624
|
*
|
|
12411
12625
|
* @module routes/metas
|
|
12412
12626
|
*/
|
|
12627
|
+
/** Reusable Zod schema for boolean query string parameters ('true'/'false'). */
|
|
12628
|
+
const boolQueryParam = z.enum(['true', 'false']).transform((v) => v === 'true');
|
|
12413
12629
|
const metasQuerySchema = z.object({
|
|
12414
12630
|
pathPrefix: z.string().optional(),
|
|
12415
|
-
hasError:
|
|
12416
|
-
.enum(['true', 'false'])
|
|
12417
|
-
.transform((v) => v === 'true')
|
|
12418
|
-
.optional(),
|
|
12631
|
+
hasError: boolQueryParam.optional(),
|
|
12419
12632
|
staleHours: z
|
|
12420
12633
|
.string()
|
|
12421
12634
|
.transform(Number)
|
|
12422
12635
|
.pipe(z.number().positive())
|
|
12423
12636
|
.optional(),
|
|
12424
|
-
neverSynthesized:
|
|
12425
|
-
|
|
12426
|
-
|
|
12427
|
-
.optional(),
|
|
12428
|
-
locked: z
|
|
12429
|
-
.enum(['true', 'false'])
|
|
12430
|
-
.transform((v) => v === 'true')
|
|
12431
|
-
.optional(),
|
|
12432
|
-
disabled: z
|
|
12433
|
-
.enum(['true', 'false'])
|
|
12434
|
-
.transform((v) => v === 'true')
|
|
12435
|
-
.optional(),
|
|
12637
|
+
neverSynthesized: boolQueryParam.optional(),
|
|
12638
|
+
locked: boolQueryParam.optional(),
|
|
12639
|
+
disabled: boolQueryParam.optional(),
|
|
12436
12640
|
fields: z.string().optional(),
|
|
12437
12641
|
});
|
|
12438
12642
|
const metaDetailQuerySchema = z.object({
|
|
12439
12643
|
fields: z.string().optional(),
|
|
12440
12644
|
includeArchive: z
|
|
12441
12645
|
.union([
|
|
12442
|
-
|
|
12646
|
+
boolQueryParam,
|
|
12443
12647
|
z.string().transform(Number).pipe(z.number().int().nonnegative()),
|
|
12444
12648
|
])
|
|
12445
12649
|
.optional(),
|
|
12446
12650
|
});
|
|
12447
12651
|
function registerMetasRoutes(app, deps) {
|
|
12448
|
-
app.get('
|
|
12652
|
+
app.get(getEndpoint('listMetas').path, async (request) => {
|
|
12449
12653
|
const query = metasQuerySchema.parse(request.query);
|
|
12450
12654
|
const { config, watcher } = deps;
|
|
12451
12655
|
const result = await listMetas(config, watcher);
|
|
@@ -12517,7 +12721,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
12517
12721
|
});
|
|
12518
12722
|
return { summary, metas };
|
|
12519
12723
|
});
|
|
12520
|
-
app.get('
|
|
12724
|
+
app.get(getEndpoint('metaDetail').path, async (request, reply) => {
|
|
12521
12725
|
const query = metaDetailQuerySchema.parse(request.query);
|
|
12522
12726
|
const { config, watcher } = deps;
|
|
12523
12727
|
const targetPath = normalizePath(decodeURIComponent(request.params.path));
|
|
@@ -12611,7 +12815,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
12611
12815
|
/**
|
|
12612
12816
|
* PATCH /metas/:path — update user-settable reserved properties on a meta.
|
|
12613
12817
|
*
|
|
12614
|
-
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled.
|
|
12818
|
+
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled, _architectTimeout, _builderTimeout, _criticTimeout.
|
|
12615
12819
|
* Set a field to null to remove it. Unknown keys are rejected.
|
|
12616
12820
|
*
|
|
12617
12821
|
* @module routes/metasUpdate
|
|
@@ -12623,10 +12827,13 @@ const updateBodySchema = z
|
|
|
12623
12827
|
_depth: z.union([z.number(), z.null()]).optional(),
|
|
12624
12828
|
_crossRefs: z.union([z.array(z.string()), z.null()]).optional(),
|
|
12625
12829
|
_disabled: z.union([z.boolean(), z.null()]).optional(),
|
|
12830
|
+
_architectTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
12831
|
+
_builderTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
12832
|
+
_criticTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
12626
12833
|
})
|
|
12627
12834
|
.strict();
|
|
12628
12835
|
function registerMetasUpdateRoute(app, deps) {
|
|
12629
|
-
app.patch('
|
|
12836
|
+
app.patch(getEndpoint('updateMeta').path, async (request, reply) => {
|
|
12630
12837
|
const parseResult = updateBodySchema.safeParse(request.body);
|
|
12631
12838
|
if (!parseResult.success) {
|
|
12632
12839
|
return reply.status(400).send({
|
|
@@ -12648,13 +12855,7 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12648
12855
|
});
|
|
12649
12856
|
}
|
|
12650
12857
|
const metaJsonPath = join(metaDir, 'meta.json');
|
|
12651
|
-
const KEYS =
|
|
12652
|
-
'_steer',
|
|
12653
|
-
'_emphasis',
|
|
12654
|
-
'_depth',
|
|
12655
|
-
'_crossRefs',
|
|
12656
|
-
'_disabled',
|
|
12657
|
-
];
|
|
12858
|
+
const KEYS = Object.keys(updateBodySchema.shape);
|
|
12658
12859
|
const toDelete = new Set();
|
|
12659
12860
|
const toSet = {};
|
|
12660
12861
|
for (const key of KEYS) {
|
|
@@ -12692,7 +12893,7 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12692
12893
|
* @module routes/preview
|
|
12693
12894
|
*/
|
|
12694
12895
|
function registerPreviewRoute(app, deps) {
|
|
12695
|
-
app.get('
|
|
12896
|
+
app.get(getEndpoint('preview').path, async (request, reply) => {
|
|
12696
12897
|
const { config, watcher, cache } = deps;
|
|
12697
12898
|
const query = request.query;
|
|
12698
12899
|
let result;
|
|
@@ -12730,12 +12931,8 @@ function registerPreviewRoute(app, deps) {
|
|
|
12730
12931
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
12731
12932
|
// Compute invalidation inputs (DRY: reuse phaseState/invalidate logic)
|
|
12732
12933
|
const invalidation = await computeInvalidation(meta, scopeFiles, config, targetNode);
|
|
12733
|
-
const { architectInvalidators,
|
|
12734
|
-
const
|
|
12735
|
-
const structureChanged = structureHash !== meta._structureHash;
|
|
12736
|
-
const { steerChanged } = invalidation;
|
|
12737
|
-
const { architectChanged, crossRefsDeclChanged } = stalenessInputs;
|
|
12738
|
-
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
12934
|
+
const { architectInvalidators, inputStatus, phaseState } = invalidation;
|
|
12935
|
+
const architectTriggered = architectInvalidators.length > 0;
|
|
12739
12936
|
// Delta files
|
|
12740
12937
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
12741
12938
|
// EMA token estimates
|
|
@@ -12749,14 +12946,6 @@ function registerPreviewRoute(app, deps) {
|
|
|
12749
12946
|
? Math.round((Date.now() - new Date(meta._generatedAt).getTime()) / 1000)
|
|
12750
12947
|
: null;
|
|
12751
12948
|
const stalenessScore = computeStalenessScore(stalenessSeconds, meta._depth ?? 0, meta._emphasis ?? 1, config.depthWeight);
|
|
12752
|
-
// Phase state
|
|
12753
|
-
const phaseState = derivePhaseState(meta, {
|
|
12754
|
-
structureChanged,
|
|
12755
|
-
steerChanged,
|
|
12756
|
-
architectChanged,
|
|
12757
|
-
crossRefsChanged: crossRefsDeclChanged,
|
|
12758
|
-
architectEvery: config.architectEvery,
|
|
12759
|
-
});
|
|
12760
12949
|
const owedPhase = getOwedPhase(phaseState);
|
|
12761
12950
|
const priorityBand = getPriorityBand(phaseState);
|
|
12762
12951
|
return {
|
|
@@ -12767,10 +12956,17 @@ function registerPreviewRoute(app, deps) {
|
|
|
12767
12956
|
},
|
|
12768
12957
|
architectWillRun: architectTriggered,
|
|
12769
12958
|
architectReason: [
|
|
12770
|
-
...(
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
...((
|
|
12959
|
+
...(architectInvalidators.includes('firstRun')
|
|
12960
|
+
? ['no cached builder (first run)']
|
|
12961
|
+
: []),
|
|
12962
|
+
...(architectInvalidators.includes('structureHash')
|
|
12963
|
+
? ['structure changed']
|
|
12964
|
+
: []),
|
|
12965
|
+
...(architectInvalidators.includes('steer') ? ['steer changed'] : []),
|
|
12966
|
+
...(architectInvalidators.includes('_crossRefs')
|
|
12967
|
+
? ['cross-refs changed']
|
|
12968
|
+
: []),
|
|
12969
|
+
...(architectInvalidators.includes('architectEvery')
|
|
12774
12970
|
? ['periodic refresh']
|
|
12775
12971
|
: []),
|
|
12776
12972
|
].join(', ') || 'not triggered',
|
|
@@ -12787,7 +12983,7 @@ function registerPreviewRoute(app, deps) {
|
|
|
12787
12983
|
owedPhase,
|
|
12788
12984
|
priorityBand,
|
|
12789
12985
|
phaseState,
|
|
12790
|
-
|
|
12986
|
+
inputStatus,
|
|
12791
12987
|
architectInvalidators,
|
|
12792
12988
|
};
|
|
12793
12989
|
});
|
|
@@ -12805,7 +13001,7 @@ function registerPreviewRoute(app, deps) {
|
|
|
12805
13001
|
/** Register queue management routes. */
|
|
12806
13002
|
function registerQueueRoutes(app, deps) {
|
|
12807
13003
|
const { queue } = deps;
|
|
12808
|
-
app.get('
|
|
13004
|
+
app.get(getEndpoint('queue').path, async () => {
|
|
12809
13005
|
const currentPhase = queue.currentPhase;
|
|
12810
13006
|
const overrides = queue.overrides;
|
|
12811
13007
|
// Compute owedPhase for each override entry by reading meta state
|
|
@@ -12881,11 +13077,11 @@ function registerQueueRoutes(app, deps) {
|
|
|
12881
13077
|
state: queue.getState(),
|
|
12882
13078
|
};
|
|
12883
13079
|
});
|
|
12884
|
-
app.post('
|
|
13080
|
+
app.post(getEndpoint('queueClear').path, () => {
|
|
12885
13081
|
const removed = queue.clearOverrides();
|
|
12886
13082
|
return { cleared: removed };
|
|
12887
13083
|
});
|
|
12888
|
-
app.post('
|
|
13084
|
+
app.post(getEndpoint('abort').path, async (_request, reply) => {
|
|
12889
13085
|
// Check 3-layer current first
|
|
12890
13086
|
const currentPhase = queue.currentPhase;
|
|
12891
13087
|
const current = currentPhase ?? queue.current;
|
|
@@ -12947,7 +13143,7 @@ const seedBodySchema = z.object({
|
|
|
12947
13143
|
steer: z.string().optional(),
|
|
12948
13144
|
});
|
|
12949
13145
|
function registerSeedRoute(app, deps) {
|
|
12950
|
-
app.post('
|
|
13146
|
+
app.post(getEndpoint('seed').path, async (request, reply) => {
|
|
12951
13147
|
const body = seedBodySchema.parse(request.body);
|
|
12952
13148
|
if (metaExists(body.path)) {
|
|
12953
13149
|
return reply.status(409).send({
|
|
@@ -13112,7 +13308,7 @@ function registerStatusRoute(app, deps) {
|
|
|
13112
13308
|
dependencies: {
|
|
13113
13309
|
watcher: {
|
|
13114
13310
|
...watcherHealth,
|
|
13115
|
-
rulesRegistered:
|
|
13311
|
+
rulesRegistered: true,
|
|
13116
13312
|
},
|
|
13117
13313
|
gateway: gatewayHealth,
|
|
13118
13314
|
},
|
|
@@ -13121,7 +13317,7 @@ function registerStatusRoute(app, deps) {
|
|
|
13121
13317
|
};
|
|
13122
13318
|
},
|
|
13123
13319
|
});
|
|
13124
|
-
app.get('
|
|
13320
|
+
app.get(getEndpoint('status').path, async (_request, reply) => {
|
|
13125
13321
|
const result = await statusHandler();
|
|
13126
13322
|
return reply.status(result.status).send(result.body);
|
|
13127
13323
|
});
|
|
@@ -13140,24 +13336,41 @@ const synthesizeBodySchema = z.object({
|
|
|
13140
13336
|
});
|
|
13141
13337
|
/** Register the POST /synthesize route. */
|
|
13142
13338
|
function registerSynthesizeRoute(app, deps) {
|
|
13143
|
-
app.post('
|
|
13339
|
+
app.post(getEndpoint('synthesize').path, async (request, reply) => {
|
|
13144
13340
|
const body = synthesizeBodySchema.parse(request.body);
|
|
13145
13341
|
const { config, watcher, queue, cache } = deps;
|
|
13146
13342
|
if (body.path) {
|
|
13147
13343
|
// Path-targeted trigger: create override entry
|
|
13148
13344
|
const targetPath = resolveMetaDir(body.path);
|
|
13149
|
-
// Read meta
|
|
13345
|
+
// Read meta and recompute invalidation against current inputs
|
|
13346
|
+
// (structure hash, steer, cross-refs, prompt snapshots) rather than
|
|
13347
|
+
// trusting the cached _phaseState. Fixes #160.
|
|
13150
13348
|
let owedPhase = null;
|
|
13151
13349
|
let meta;
|
|
13152
13350
|
try {
|
|
13153
13351
|
meta = await readMetaJson(targetPath);
|
|
13154
|
-
const
|
|
13155
|
-
|
|
13352
|
+
const node = await buildMinimalNode(normalizePath(targetPath), watcher);
|
|
13353
|
+
const { scopeFiles } = await getScopeFiles(node, watcher);
|
|
13354
|
+
const invalidation = await computeInvalidation(meta, scopeFiles, config, node);
|
|
13355
|
+
owedPhase = getOwedPhase(invalidation.phaseState);
|
|
13356
|
+
// Persist recomputed phase state + structure hash when stale.
|
|
13357
|
+
// Matches the scheduler's Tier 2 pattern: always persist so the
|
|
13358
|
+
// stored _phaseState reflects reality for subsequent reads.
|
|
13359
|
+
if (owedPhase) {
|
|
13360
|
+
await persistPhaseState({
|
|
13361
|
+
metaPath: targetPath,
|
|
13362
|
+
current: meta,
|
|
13363
|
+
config,
|
|
13364
|
+
structureHash: invalidation.inputStatus.structureHash,
|
|
13365
|
+
}, invalidation.phaseState, {});
|
|
13366
|
+
cache.invalidate();
|
|
13367
|
+
}
|
|
13156
13368
|
}
|
|
13157
13369
|
catch {
|
|
13158
|
-
// Meta unreadable — proceed,
|
|
13370
|
+
// Meta unreadable or watcher unavailable — proceed,
|
|
13371
|
+
// phase will be evaluated at dequeue time
|
|
13159
13372
|
}
|
|
13160
|
-
// Fully fresh meta → skip
|
|
13373
|
+
// Fully fresh meta → skip
|
|
13161
13374
|
if (owedPhase === null && meta && (meta._phaseState || meta._content)) {
|
|
13162
13375
|
return await reply.code(200).send({
|
|
13163
13376
|
status: 'skipped',
|
|
@@ -13215,7 +13428,7 @@ const unlockBodySchema = z.object({
|
|
|
13215
13428
|
path: z.string().min(1),
|
|
13216
13429
|
});
|
|
13217
13430
|
function registerUnlockRoute(app, deps) {
|
|
13218
|
-
app.post('
|
|
13431
|
+
app.post(getEndpoint('unlock').path, (request, reply) => {
|
|
13219
13432
|
const body = unlockBodySchema.parse(request.body);
|
|
13220
13433
|
const metaDir = resolveMetaDir(body.path);
|
|
13221
13434
|
const lockPath = join(metaDir, '.lock');
|
|
@@ -13612,9 +13825,7 @@ async function startService(config, configPath) {
|
|
|
13612
13825
|
routeDeps.registrar = registrar;
|
|
13613
13826
|
void registrar.register().then(() => {
|
|
13614
13827
|
routeDeps.ready = true;
|
|
13615
|
-
|
|
13616
|
-
void verifyRuleApplication(watcher, logger);
|
|
13617
|
-
}
|
|
13828
|
+
void verifyRuleApplication(watcher, logger);
|
|
13618
13829
|
}, () => {
|
|
13619
13830
|
// Registration failed after max retries — mark ready anyway
|
|
13620
13831
|
routeDeps.ready = true;
|