@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
package/dist/index.js
CHANGED
|
@@ -80,6 +80,113 @@ const META_COMPONENT = {
|
|
|
80
80
|
pluginPackage: '@karmaniverous/jeeves-meta-openclaw',
|
|
81
81
|
defaultPort: 1938};
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Shared endpoint catalog — single source of truth for the jeeves-meta API.
|
|
85
|
+
*
|
|
86
|
+
* Both the CLI service and the OpenClaw plugin derive their registrations
|
|
87
|
+
* from this declarative catalog, eliminating drift between the two.
|
|
88
|
+
*
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* Canonical endpoint catalog for the jeeves-meta API.
|
|
92
|
+
*
|
|
93
|
+
* Every entry describes a single HTTP endpoint exposed by the service.
|
|
94
|
+
* Route handlers, plugin tools, and HTTP clients should reference these
|
|
95
|
+
* descriptors rather than hard-coding paths and descriptions.
|
|
96
|
+
*/
|
|
97
|
+
const META_ENDPOINTS = [
|
|
98
|
+
{
|
|
99
|
+
name: 'status',
|
|
100
|
+
method: 'GET',
|
|
101
|
+
path: '/status',
|
|
102
|
+
description: 'Service health and status overview.',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'listMetas',
|
|
106
|
+
method: 'GET',
|
|
107
|
+
path: '/metas',
|
|
108
|
+
description: 'List metas with summary stats and per-meta projection. Response includes _phaseState and owedPhase per meta.',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'metaDetail',
|
|
112
|
+
method: 'GET',
|
|
113
|
+
path: '/metas/:path',
|
|
114
|
+
description: 'Full detail for a single meta, with optional archive history. Response includes _phaseState and owedPhase.',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'updateMeta',
|
|
118
|
+
method: 'PATCH',
|
|
119
|
+
path: '/metas/:path',
|
|
120
|
+
description: 'Update user-settable reserved properties on a meta entity.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'synthesize',
|
|
124
|
+
method: 'POST',
|
|
125
|
+
path: '/synthesize',
|
|
126
|
+
description: 'Trigger synthesis. Path-targeted creates an override queue entry; returns owedPhase. Fully-fresh metas return status:skipped.',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'abort',
|
|
130
|
+
method: 'POST',
|
|
131
|
+
path: '/synthesize/abort',
|
|
132
|
+
description: 'Abort the currently running synthesis.',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'preview',
|
|
136
|
+
method: 'GET',
|
|
137
|
+
path: '/preview',
|
|
138
|
+
description: 'Dry-run preview of next synthesis. Returns owedPhase, priorityBand, phaseState, inputStatus, and architectInvalidators.',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'seed',
|
|
142
|
+
method: 'POST',
|
|
143
|
+
path: '/seed',
|
|
144
|
+
description: 'Create a .meta/ directory and initial meta.json for a new entity path.',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'unlock',
|
|
148
|
+
method: 'POST',
|
|
149
|
+
path: '/unlock',
|
|
150
|
+
description: 'Remove a stale .lock from a meta entity that is stuck.',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'config',
|
|
154
|
+
method: 'GET',
|
|
155
|
+
path: '/config',
|
|
156
|
+
description: 'Query service configuration with optional JSONPath.',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'configApply',
|
|
160
|
+
method: 'POST',
|
|
161
|
+
path: '/config/apply',
|
|
162
|
+
description: 'Apply a configuration patch.',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'queue',
|
|
166
|
+
method: 'GET',
|
|
167
|
+
path: '/queue',
|
|
168
|
+
description: 'List queued synthesis operations (3-layer model: current, overrides, automatic).',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'queueClear',
|
|
172
|
+
method: 'POST',
|
|
173
|
+
path: '/queue/clear',
|
|
174
|
+
description: 'Clear override entries from the queue.',
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
/**
|
|
178
|
+
* Look up an endpoint descriptor by name.
|
|
179
|
+
*
|
|
180
|
+
* @param name - The endpoint identifier.
|
|
181
|
+
* @returns The matching {@link EndpointDescriptor}.
|
|
182
|
+
*/
|
|
183
|
+
function getEndpoint(name) {
|
|
184
|
+
const ep = META_ENDPOINTS.find((e) => e.name === name);
|
|
185
|
+
if (!ep)
|
|
186
|
+
throw new Error(`Unknown endpoint: ${name}`);
|
|
187
|
+
return ep;
|
|
188
|
+
}
|
|
189
|
+
|
|
83
190
|
/**
|
|
84
191
|
* Structured error schema from a synthesis step failure.
|
|
85
192
|
*
|
|
@@ -6018,6 +6125,9 @@ function requireRange () {
|
|
|
6018
6125
|
}
|
|
6019
6126
|
|
|
6020
6127
|
parseRange (range) {
|
|
6128
|
+
// strip build metadata so it can't bleed into the version
|
|
6129
|
+
range = range.replace(BUILDSTRIPRE, '');
|
|
6130
|
+
|
|
6021
6131
|
// memoize range parsing for performance.
|
|
6022
6132
|
// this is a very hot path, and fully deterministic.
|
|
6023
6133
|
const memoOpts =
|
|
@@ -6143,6 +6253,7 @@ function requireRange () {
|
|
|
6143
6253
|
const SemVer = requireSemver$1();
|
|
6144
6254
|
const {
|
|
6145
6255
|
safeRe: re,
|
|
6256
|
+
src,
|
|
6146
6257
|
t,
|
|
6147
6258
|
comparatorTrimReplace,
|
|
6148
6259
|
tildeTrimReplace,
|
|
@@ -6150,6 +6261,9 @@ function requireRange () {
|
|
|
6150
6261
|
} = requireRe();
|
|
6151
6262
|
const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = requireConstants();
|
|
6152
6263
|
|
|
6264
|
+
// unbounded global build-metadata stripper used by parseRange
|
|
6265
|
+
const BUILDSTRIPRE = new RegExp(src[t.BUILD], 'g');
|
|
6266
|
+
|
|
6153
6267
|
const isNullSet = c => c.value === '<0.0.0-0';
|
|
6154
6268
|
const isAny = c => c.value === '';
|
|
6155
6269
|
|
|
@@ -7201,7 +7315,7 @@ function requireSubset () {
|
|
|
7201
7315
|
if (higher === c && higher !== gt) {
|
|
7202
7316
|
return false
|
|
7203
7317
|
}
|
|
7204
|
-
} else if (gt.operator === '>=' && !
|
|
7318
|
+
} else if (gt.operator === '>=' && !c.test(gt.semver)) {
|
|
7205
7319
|
return false
|
|
7206
7320
|
}
|
|
7207
7321
|
}
|
|
@@ -7219,7 +7333,7 @@ function requireSubset () {
|
|
|
7219
7333
|
if (lower === c && lower !== lt) {
|
|
7220
7334
|
return false
|
|
7221
7335
|
}
|
|
7222
|
-
} else if (lt.operator === '<=' && !
|
|
7336
|
+
} else if (lt.operator === '<=' && !c.test(lt.semver)) {
|
|
7223
7337
|
return false
|
|
7224
7338
|
}
|
|
7225
7339
|
}
|
|
@@ -7879,6 +7993,20 @@ When editing files outside the workspace, use the bridge pattern: copy in → ed
|
|
|
7879
7993
|
|
|
7880
7994
|
**Cross-channel sends:** Use the \`message\` tool with an explicit \`target\` to send to a different channel or DM.
|
|
7881
7995
|
|
|
7996
|
+
### Slack File Downloads
|
|
7997
|
+
|
|
7998
|
+
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:
|
|
7999
|
+
|
|
8000
|
+
\`\`\`js
|
|
8001
|
+
fetch(url_private_download, {
|
|
8002
|
+
headers: { Authorization: 'Bearer ' + botToken },
|
|
8003
|
+
});
|
|
8004
|
+
\`\`\`
|
|
8005
|
+
|
|
8006
|
+
The bot token is at \`channels.slack.accounts.default.botToken\` in \`openclaw.json\`.
|
|
8007
|
+
|
|
8008
|
+
Never tell the user a file can't be downloaded until both methods have been tried.
|
|
8009
|
+
|
|
7882
8010
|
### Plugin Lifecycle
|
|
7883
8011
|
|
|
7884
8012
|
\`\`\`bash
|
|
@@ -8361,6 +8489,24 @@ async function discoverMetas(watcher) {
|
|
|
8361
8489
|
return metaPaths;
|
|
8362
8490
|
}
|
|
8363
8491
|
|
|
8492
|
+
/**
|
|
8493
|
+
* Retrieve the nearest ancestor meta node from the ownership tree.
|
|
8494
|
+
*
|
|
8495
|
+
* @module discovery/getAncestorMeta
|
|
8496
|
+
*/
|
|
8497
|
+
/**
|
|
8498
|
+
* Get the nearest ancestor MetaNode for a given node.
|
|
8499
|
+
*
|
|
8500
|
+
* Walks up the ownership tree (via the parent pointer set by
|
|
8501
|
+
* buildOwnershipTree) to find the closest ancestor .meta/ directory.
|
|
8502
|
+
*
|
|
8503
|
+
* @param node - The meta node to find the ancestor for.
|
|
8504
|
+
* @returns The parent MetaNode, or null for root-level metas.
|
|
8505
|
+
*/
|
|
8506
|
+
function getAncestorMeta(node) {
|
|
8507
|
+
return node.parent;
|
|
8508
|
+
}
|
|
8509
|
+
|
|
8364
8510
|
/**
|
|
8365
8511
|
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
8366
8512
|
*
|
|
@@ -9033,6 +9179,14 @@ const autoSeedRuleSchema = z.object({
|
|
|
9033
9179
|
steer: z.string().optional(),
|
|
9034
9180
|
/** Optional cross-references for seeded metas. */
|
|
9035
9181
|
crossRefs: z.array(z.string()).optional(),
|
|
9182
|
+
/** Walk up this many extra parent levels from the matched file's directory. Default 0. */
|
|
9183
|
+
parentDepth: z.number().int().min(0).optional(),
|
|
9184
|
+
/** Per-category timeout override for the architect phase (seconds, min 30). */
|
|
9185
|
+
architectTimeout: z.number().int().min(30).optional(),
|
|
9186
|
+
/** Per-category timeout override for the builder phase (seconds, min 30). */
|
|
9187
|
+
builderTimeout: z.number().int().min(30).optional(),
|
|
9188
|
+
/** Per-category timeout override for the critic phase (seconds, min 30). */
|
|
9189
|
+
criticTimeout: z.number().int().min(30).optional(),
|
|
9036
9190
|
});
|
|
9037
9191
|
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
9038
9192
|
const serviceConfigSchema = metaConfigSchema.extend({
|
|
@@ -9050,6 +9204,8 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
9050
9204
|
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
9051
9205
|
/** Logging configuration. */
|
|
9052
9206
|
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
9207
|
+
/** Max number of all-fresh candidates to scan per tick in Tier 2 invalidation. */
|
|
9208
|
+
tier2ScanLimit: z.number().int().min(1).default(50),
|
|
9053
9209
|
/**
|
|
9054
9210
|
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
9055
9211
|
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
@@ -9322,7 +9478,7 @@ class GatewayExecutor {
|
|
|
9322
9478
|
'Write your complete output to a file using the Write tool at:\n' +
|
|
9323
9479
|
outputPath +
|
|
9324
9480
|
'\n\n' +
|
|
9325
|
-
'After writing the file,
|
|
9481
|
+
'After writing the file, your final message must be exactly: ANNOUNCE_SKIP';
|
|
9326
9482
|
// Step 1: Spawn the sub-agent session (unique label per cycle to avoid
|
|
9327
9483
|
// "label already in use" errors — gateway labels persist after session completion)
|
|
9328
9484
|
const labelBase = options?.label ?? 'jeeves-meta-synthesis';
|
|
@@ -9390,7 +9546,9 @@ class GatewayExecutor {
|
|
|
9390
9546
|
}
|
|
9391
9547
|
}
|
|
9392
9548
|
}
|
|
9393
|
-
// Fallback: extract from message content if file wasn't written
|
|
9549
|
+
// Fallback: extract from message content if file wasn't written.
|
|
9550
|
+
// Skip ANNOUNCE_SKIP sentinel messages — the real output is in
|
|
9551
|
+
// a preceding assistant message (the file write).
|
|
9394
9552
|
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9395
9553
|
const msg = msgArray[i];
|
|
9396
9554
|
if (msg.role === 'assistant' && msg.content) {
|
|
@@ -9402,7 +9560,7 @@ class GatewayExecutor {
|
|
|
9402
9560
|
.map((b) => b.text)
|
|
9403
9561
|
.join('\n')
|
|
9404
9562
|
: '';
|
|
9405
|
-
if (text)
|
|
9563
|
+
if (text && text.trim() !== 'ANNOUNCE_SKIP')
|
|
9406
9564
|
return { output: text, tokens };
|
|
9407
9565
|
}
|
|
9408
9566
|
}
|
|
@@ -9432,11 +9590,16 @@ class GatewayExecutor {
|
|
|
9432
9590
|
function createLogger(config) {
|
|
9433
9591
|
const level = config?.level ?? 'info';
|
|
9434
9592
|
if (config?.file) {
|
|
9435
|
-
const
|
|
9436
|
-
|
|
9437
|
-
|
|
9593
|
+
const fileStream = pino.destination({
|
|
9594
|
+
dest: config.file,
|
|
9595
|
+
sync: false,
|
|
9596
|
+
mkdir: true,
|
|
9438
9597
|
});
|
|
9439
|
-
|
|
9598
|
+
const multistream = pino.multistream([
|
|
9599
|
+
{ stream: process.stdout },
|
|
9600
|
+
{ stream: fileStream },
|
|
9601
|
+
]);
|
|
9602
|
+
return pino({ level }, multistream);
|
|
9440
9603
|
}
|
|
9441
9604
|
return pino({ level });
|
|
9442
9605
|
}
|
|
@@ -9558,6 +9721,21 @@ async function buildContextPackage(node, meta, watcher, logger) {
|
|
|
9558
9721
|
}
|
|
9559
9722
|
// Archive paths
|
|
9560
9723
|
const archives = listArchiveFiles(node.metaPath);
|
|
9724
|
+
// Nearest ancestor _builder output
|
|
9725
|
+
let ancestorBuilder;
|
|
9726
|
+
const ancestor = getAncestorMeta(node);
|
|
9727
|
+
if (ancestor) {
|
|
9728
|
+
try {
|
|
9729
|
+
const raw = await readFile(join(ancestor.metaPath, 'meta.json'), 'utf8');
|
|
9730
|
+
const ancestorMeta = JSON.parse(raw);
|
|
9731
|
+
if (ancestorMeta._builder) {
|
|
9732
|
+
ancestorBuilder = ancestorMeta._builder;
|
|
9733
|
+
}
|
|
9734
|
+
}
|
|
9735
|
+
catch {
|
|
9736
|
+
// Ancestor meta.json unreadable — skip
|
|
9737
|
+
}
|
|
9738
|
+
}
|
|
9561
9739
|
return {
|
|
9562
9740
|
path: node.metaPath,
|
|
9563
9741
|
scopeFiles,
|
|
@@ -9569,6 +9747,7 @@ async function buildContextPackage(node, meta, watcher, logger) {
|
|
|
9569
9747
|
steer: meta._steer ?? null,
|
|
9570
9748
|
previousState: meta._state ?? null,
|
|
9571
9749
|
archives,
|
|
9750
|
+
ancestorBuilder,
|
|
9572
9751
|
};
|
|
9573
9752
|
}
|
|
9574
9753
|
|
|
@@ -9616,6 +9795,12 @@ function appendMetaSections(sections, heading, metas) {
|
|
|
9616
9795
|
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
9617
9796
|
}
|
|
9618
9797
|
}
|
|
9798
|
+
/** Inject nearest ancestor's organizational context, if available. */
|
|
9799
|
+
function appendAncestorContext(sections, ctx) {
|
|
9800
|
+
if (ctx.ancestorBuilder) {
|
|
9801
|
+
sections.push('', '## PARENT ORGANIZATIONAL CONTEXT', ctx.ancestorBuilder);
|
|
9802
|
+
}
|
|
9803
|
+
}
|
|
9619
9804
|
/** Append optional context sections shared across all step prompts. */
|
|
9620
9805
|
function appendSharedSections(sections, ctx, options) {
|
|
9621
9806
|
const opts = {
|
|
@@ -9655,7 +9840,7 @@ function buildArchitectTask(ctx, meta, config) {
|
|
|
9655
9840
|
const sections = [
|
|
9656
9841
|
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
9657
9842
|
'',
|
|
9658
|
-
|
|
9843
|
+
config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
|
|
9659
9844
|
'',
|
|
9660
9845
|
'## SCOPE',
|
|
9661
9846
|
`Path: ${ctx.path}`,
|
|
@@ -9665,6 +9850,7 @@ function buildArchitectTask(ctx, meta, config) {
|
|
|
9665
9850
|
'### File listing (scope)',
|
|
9666
9851
|
condenseScopeFiles(ctx.scopeFiles),
|
|
9667
9852
|
];
|
|
9853
|
+
appendAncestorContext(sections, ctx);
|
|
9668
9854
|
// Inject previous _builder so architect can see its own prior output
|
|
9669
9855
|
if (meta._builder) {
|
|
9670
9856
|
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
@@ -9695,6 +9881,7 @@ function buildBuilderTask(ctx, meta, config) {
|
|
|
9695
9881
|
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
9696
9882
|
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
9697
9883
|
];
|
|
9884
|
+
appendAncestorContext(sections, ctx);
|
|
9698
9885
|
if (ctx.previousState != null) {
|
|
9699
9886
|
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), '```');
|
|
9700
9887
|
}
|
|
@@ -9717,7 +9904,7 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
9717
9904
|
const sections = [
|
|
9718
9905
|
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
9719
9906
|
'',
|
|
9720
|
-
|
|
9907
|
+
config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
9721
9908
|
'',
|
|
9722
9909
|
'## SYNTHESIS TO EVALUATE',
|
|
9723
9910
|
meta._content ?? '(No content produced)',
|
|
@@ -9851,7 +10038,7 @@ function enforceInvariant(state) {
|
|
|
9851
10038
|
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
9852
10039
|
/**
|
|
9853
10040
|
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
9854
|
-
* Triggers:
|
|
10041
|
+
* Triggers: first run, _structureHash change, _steer change,
|
|
9855
10042
|
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
9856
10043
|
*/
|
|
9857
10044
|
function invalidateArchitect(state) {
|
|
@@ -10049,31 +10236,6 @@ function derivePhaseState(meta, inputs) {
|
|
|
10049
10236
|
if (!meta._content && !meta._builder) {
|
|
10050
10237
|
return initialPhaseState();
|
|
10051
10238
|
}
|
|
10052
|
-
// Check architect invalidation (when inputs are provided)
|
|
10053
|
-
if (inputs) {
|
|
10054
|
-
// Progressive metas: structure changes invalidate builder, not architect
|
|
10055
|
-
const structureInvalidatesArchitect = inputs.structureChanged && meta._state === undefined;
|
|
10056
|
-
const architectInvalidated = structureInvalidatesArchitect ||
|
|
10057
|
-
inputs.steerChanged ||
|
|
10058
|
-
inputs.architectChanged ||
|
|
10059
|
-
inputs.crossRefsChanged ||
|
|
10060
|
-
(meta._synthesisCount ?? 0) >= inputs.architectEvery;
|
|
10061
|
-
if (architectInvalidated) {
|
|
10062
|
-
return {
|
|
10063
|
-
architect: 'pending',
|
|
10064
|
-
builder: 'stale',
|
|
10065
|
-
critic: 'stale',
|
|
10066
|
-
};
|
|
10067
|
-
}
|
|
10068
|
-
// Progressive meta with structure change: builder-only invalidation
|
|
10069
|
-
if (inputs.structureChanged && meta._state !== undefined) {
|
|
10070
|
-
return {
|
|
10071
|
-
architect: 'fresh',
|
|
10072
|
-
builder: 'pending',
|
|
10073
|
-
critic: 'stale',
|
|
10074
|
-
};
|
|
10075
|
-
}
|
|
10076
|
-
}
|
|
10077
10239
|
// Has _builder but no _content: builder is pending
|
|
10078
10240
|
if (meta._builder && !meta._content) {
|
|
10079
10241
|
return {
|
|
@@ -10122,6 +10284,14 @@ function computeStructureHash(filePaths) {
|
|
|
10122
10284
|
*
|
|
10123
10285
|
* @module phaseState/invalidate
|
|
10124
10286
|
*/
|
|
10287
|
+
/**
|
|
10288
|
+
* Check whether a persisted prompt snapshot mismatches the currently-resolved prompt.
|
|
10289
|
+
* Returns true when the snapshot exists and differs from the resolved prompt.
|
|
10290
|
+
* This is informational only — it does NOT trigger invalidation.
|
|
10291
|
+
*/
|
|
10292
|
+
function isPromptStale(snapshot, resolved) {
|
|
10293
|
+
return snapshot !== undefined && snapshot !== resolved;
|
|
10294
|
+
}
|
|
10125
10295
|
/**
|
|
10126
10296
|
* Compute invalidation inputs and apply cascade for a single meta.
|
|
10127
10297
|
*
|
|
@@ -10144,10 +10314,16 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
|
|
|
10144
10314
|
const structureChanged = structureHash !== meta._structureHash;
|
|
10145
10315
|
const latestArchive = await readLatestArchive(node.metaPath);
|
|
10146
10316
|
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10147
|
-
//
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10317
|
+
// Prompt staleness detection: compare persisted prompt snapshots against
|
|
10318
|
+
// currently-resolved prompts. This is INFORMATIONAL ONLY — reported via
|
|
10319
|
+
// inputStatus so /preview can surface it, but it must NEVER feed into
|
|
10320
|
+
// the invalidation cascade. When a meta naturally reaches architectEvery
|
|
10321
|
+
// through real builder cycles, architect runs with the current prompt and
|
|
10322
|
+
// the snapshot updates. Coupling prompt changes to invalidation causes a
|
|
10323
|
+
// corpus-wide synthesis storm (see #163).
|
|
10324
|
+
const architectChanged = isPromptStale(meta._architect, config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT);
|
|
10325
|
+
const criticChanged = isPromptStale(meta._critic, config.defaultCritic ?? DEFAULT_CRITIC_PROMPT);
|
|
10326
|
+
const effectiveSynthesisCount = meta._synthesisCount ?? 0;
|
|
10151
10327
|
// _crossRefs declaration change
|
|
10152
10328
|
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
10153
10329
|
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
@@ -10169,38 +10345,30 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
|
|
|
10169
10345
|
}
|
|
10170
10346
|
if (steerChanged)
|
|
10171
10347
|
architectInvalidators.push('steer');
|
|
10172
|
-
if (architectChanged)
|
|
10173
|
-
architectInvalidators.push('_architect');
|
|
10174
10348
|
if (crossRefsDeclChanged)
|
|
10175
10349
|
architectInvalidators.push('_crossRefs');
|
|
10176
|
-
if (
|
|
10350
|
+
if (effectiveSynthesisCount >= config.architectEvery) {
|
|
10177
10351
|
architectInvalidators.push('architectEvery');
|
|
10178
10352
|
}
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
if (architectInvalidators.length > 0
|
|
10353
|
+
if (!meta._builder)
|
|
10354
|
+
architectInvalidators.push('firstRun');
|
|
10355
|
+
if (architectInvalidators.length > 0) {
|
|
10182
10356
|
phaseState = invalidateArchitect(phaseState);
|
|
10183
10357
|
}
|
|
10184
10358
|
// ── Builder-level inputs ──
|
|
10185
|
-
// Scope file mtime check — if any file newer than _generatedAt
|
|
10186
|
-
const scopeMtimeMax = null;
|
|
10187
|
-
// Note: actual mtime check is done by the caller or via isStale;
|
|
10188
|
-
// here we just detect cross-ref content changes for the cascade.
|
|
10189
10359
|
// Cross-ref _content change (builder-invalidating)
|
|
10190
10360
|
let crossRefContentChanged = false;
|
|
10191
10361
|
return {
|
|
10192
10362
|
phaseState,
|
|
10193
10363
|
architectInvalidators,
|
|
10194
|
-
|
|
10364
|
+
inputStatus: {
|
|
10195
10365
|
structureHash,
|
|
10196
10366
|
steerChanged,
|
|
10197
10367
|
architectChanged,
|
|
10368
|
+
criticChanged,
|
|
10198
10369
|
crossRefsDeclChanged,
|
|
10199
|
-
scopeMtimeMax,
|
|
10200
10370
|
crossRefContentChanged,
|
|
10201
10371
|
},
|
|
10202
|
-
structureHash,
|
|
10203
|
-
steerChanged,
|
|
10204
10372
|
};
|
|
10205
10373
|
}
|
|
10206
10374
|
|
|
@@ -10340,20 +10508,14 @@ function selectPhaseCandidate(metas, depthWeight) {
|
|
|
10340
10508
|
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
10341
10509
|
}
|
|
10342
10510
|
/**
|
|
10343
|
-
* Select
|
|
10344
|
-
*
|
|
10345
|
-
* have structural or steer changes detectable only via I/O.
|
|
10346
|
-
*
|
|
10347
|
-
* @param metas - Phase candidate inputs (after Tier 1 filtering).
|
|
10348
|
-
* @returns The stalest all-fresh candidate, or null if none exist.
|
|
10511
|
+
* Select all fully-fresh, non-disabled, non-locked metas sorted by staleness
|
|
10512
|
+
* (descending — stalest first) for Tier 2 invalidation scanning.
|
|
10349
10513
|
*/
|
|
10350
|
-
function
|
|
10351
|
-
|
|
10514
|
+
function selectAllTier2Candidates(metas) {
|
|
10515
|
+
return metas
|
|
10352
10516
|
.filter((m) => !m.locked && !m.disabled && isFullyFresh(m.phaseState))
|
|
10353
|
-
.sort((a, b) => b.actualStaleness - a.actualStaleness)
|
|
10354
|
-
|
|
10355
|
-
return null;
|
|
10356
|
-
return { node: eligible[0].node, meta: eligible[0].meta };
|
|
10517
|
+
.sort((a, b) => b.actualStaleness - a.actualStaleness)
|
|
10518
|
+
.map((m) => ({ node: m.node, meta: m.meta }));
|
|
10357
10519
|
}
|
|
10358
10520
|
|
|
10359
10521
|
/**
|
|
@@ -10386,6 +10548,15 @@ function toMetaError(step, err, code = 'FAILED') {
|
|
|
10386
10548
|
*
|
|
10387
10549
|
* @module orchestrator/parseOutput
|
|
10388
10550
|
*/
|
|
10551
|
+
/** Sentinel appended by synthesis workers to skip the announce turn. */
|
|
10552
|
+
const ANNOUNCE_SKIP = 'ANNOUNCE_SKIP';
|
|
10553
|
+
/** Strip a trailing ANNOUNCE_SKIP sentinel from raw output. */
|
|
10554
|
+
function stripSentinel(raw) {
|
|
10555
|
+
const trimmed = raw.trim();
|
|
10556
|
+
return trimmed.endsWith(ANNOUNCE_SKIP)
|
|
10557
|
+
? trimmed.slice(0, -ANNOUNCE_SKIP.length).trim()
|
|
10558
|
+
: trimmed;
|
|
10559
|
+
}
|
|
10389
10560
|
/**
|
|
10390
10561
|
* Parse architect output. The architect returns a task brief as text.
|
|
10391
10562
|
*
|
|
@@ -10393,7 +10564,7 @@ function toMetaError(step, err, code = 'FAILED') {
|
|
|
10393
10564
|
* @returns The task brief string.
|
|
10394
10565
|
*/
|
|
10395
10566
|
function parseArchitectOutput(output) {
|
|
10396
|
-
return output
|
|
10567
|
+
return stripSentinel(output);
|
|
10397
10568
|
}
|
|
10398
10569
|
/**
|
|
10399
10570
|
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
@@ -10404,7 +10575,7 @@ function parseArchitectOutput(output) {
|
|
|
10404
10575
|
* @returns Parsed builder output with content and structured fields.
|
|
10405
10576
|
*/
|
|
10406
10577
|
function parseBuilderOutput(output) {
|
|
10407
|
-
const trimmed = output
|
|
10578
|
+
const trimmed = stripSentinel(output);
|
|
10408
10579
|
// Strategy 1: Try to parse the entire output as JSON directly
|
|
10409
10580
|
const direct = tryParseJson(trimmed);
|
|
10410
10581
|
if (direct)
|
|
@@ -10471,7 +10642,7 @@ function tryParseJson(str) {
|
|
|
10471
10642
|
* @returns The feedback string.
|
|
10472
10643
|
*/
|
|
10473
10644
|
function parseCriticOutput(output) {
|
|
10474
|
-
return output
|
|
10645
|
+
return stripSentinel(output);
|
|
10475
10646
|
}
|
|
10476
10647
|
|
|
10477
10648
|
/**
|
|
@@ -10482,6 +10653,12 @@ function parseCriticOutput(output) {
|
|
|
10482
10653
|
*
|
|
10483
10654
|
* @module orchestrator/runPhase
|
|
10484
10655
|
*/
|
|
10656
|
+
/** Compute SHA-256 hash of ancestor _builder text for observability tracking. */
|
|
10657
|
+
function hashAncestorBuilder(ancestorBuilder) {
|
|
10658
|
+
return ancestorBuilder
|
|
10659
|
+
? createHash('sha256').update(ancestorBuilder).digest('hex')
|
|
10660
|
+
: undefined;
|
|
10661
|
+
}
|
|
10485
10662
|
/** Write updated meta with phase state via lock staging. */
|
|
10486
10663
|
async function persistPhaseState(base, phaseState, updates) {
|
|
10487
10664
|
const lockPath = join(base.metaPath, '.lock');
|
|
@@ -10525,6 +10702,12 @@ async function handlePhaseFailure(phase, err, executor, ps, base, additionalUpda
|
|
|
10525
10702
|
// ── Architect executor ─────────────────────────────────────────────────
|
|
10526
10703
|
async function runArchitect(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10527
10704
|
let ps = phaseRunning(phaseState, 'architect');
|
|
10705
|
+
const base = {
|
|
10706
|
+
metaPath: node.metaPath,
|
|
10707
|
+
current: currentMeta,
|
|
10708
|
+
config,
|
|
10709
|
+
structureHash,
|
|
10710
|
+
};
|
|
10528
10711
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10529
10712
|
try {
|
|
10530
10713
|
await onProgress?.({
|
|
@@ -10536,21 +10719,25 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
10536
10719
|
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
10537
10720
|
const result = await executor.spawn(architectTask, {
|
|
10538
10721
|
thinking: config.thinking,
|
|
10539
|
-
timeout: config.architectTimeout,
|
|
10722
|
+
timeout: currentMeta._architectTimeout ?? config.architectTimeout,
|
|
10540
10723
|
label: 'meta-architect',
|
|
10541
10724
|
});
|
|
10542
10725
|
const builderBrief = parseArchitectOutput(result.output);
|
|
10543
10726
|
const architectTokens = result.tokens;
|
|
10544
10727
|
// Architect success: architect → fresh, _synthesisCount → 0
|
|
10545
10728
|
ps = architectSuccess(ps);
|
|
10546
|
-
const
|
|
10729
|
+
const architectUpdates = {
|
|
10547
10730
|
_builder: builderBrief,
|
|
10548
|
-
_architect:
|
|
10731
|
+
_architect: config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
|
|
10549
10732
|
_synthesisCount: 0,
|
|
10550
10733
|
_architectTokens: architectTokens,
|
|
10551
10734
|
_generatedAt: new Date().toISOString(),
|
|
10552
10735
|
_error: undefined,
|
|
10553
|
-
}
|
|
10736
|
+
};
|
|
10737
|
+
const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
|
|
10738
|
+
if (ancestorHash)
|
|
10739
|
+
architectUpdates._ancestorBuilderHash = ancestorHash;
|
|
10740
|
+
const updatedMeta = await persistPhaseState(base, ps, architectUpdates);
|
|
10554
10741
|
await onProgress?.({
|
|
10555
10742
|
type: 'phase_complete',
|
|
10556
10743
|
path: node.ownerPath,
|
|
@@ -10561,16 +10748,18 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
10561
10748
|
return { executed: true, phaseState: ps, updatedMeta };
|
|
10562
10749
|
}
|
|
10563
10750
|
catch (err) {
|
|
10564
|
-
return handlePhaseFailure('architect', err, executor, ps,
|
|
10565
|
-
metaPath: node.metaPath,
|
|
10566
|
-
current: currentMeta,
|
|
10567
|
-
structureHash,
|
|
10568
|
-
});
|
|
10751
|
+
return handlePhaseFailure('architect', err, executor, ps, base);
|
|
10569
10752
|
}
|
|
10570
10753
|
}
|
|
10571
10754
|
// ── Builder executor ───────────────────────────────────────────────────
|
|
10572
10755
|
async function runBuilder(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10573
10756
|
let ps = phaseRunning(phaseState, 'builder');
|
|
10757
|
+
const base = {
|
|
10758
|
+
metaPath: node.metaPath,
|
|
10759
|
+
current: currentMeta,
|
|
10760
|
+
config,
|
|
10761
|
+
structureHash,
|
|
10762
|
+
};
|
|
10574
10763
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10575
10764
|
try {
|
|
10576
10765
|
await onProgress?.({
|
|
@@ -10582,21 +10771,25 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
10582
10771
|
const builderTask = buildBuilderTask(ctx, currentMeta, config);
|
|
10583
10772
|
const result = await executor.spawn(builderTask, {
|
|
10584
10773
|
thinking: config.thinking,
|
|
10585
|
-
timeout: config.builderTimeout,
|
|
10774
|
+
timeout: currentMeta._builderTimeout ?? config.builderTimeout,
|
|
10586
10775
|
label: 'meta-builder',
|
|
10587
10776
|
});
|
|
10588
10777
|
const builderOutput = parseBuilderOutput(result.output);
|
|
10589
10778
|
const builderTokens = result.tokens;
|
|
10590
10779
|
// Builder success: builder → fresh, critic → pending
|
|
10591
10780
|
ps = builderSuccess(ps);
|
|
10592
|
-
const
|
|
10781
|
+
const builderUpdates = {
|
|
10593
10782
|
_content: builderOutput.content,
|
|
10594
10783
|
_state: builderOutput.state,
|
|
10595
10784
|
_builderTokens: builderTokens,
|
|
10596
10785
|
_generatedAt: new Date().toISOString(),
|
|
10597
10786
|
_error: undefined,
|
|
10598
10787
|
...builderOutput.fields,
|
|
10599
|
-
}
|
|
10788
|
+
};
|
|
10789
|
+
const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
|
|
10790
|
+
if (ancestorHash)
|
|
10791
|
+
builderUpdates._ancestorBuilderHash = ancestorHash;
|
|
10792
|
+
const updatedMeta = await persistPhaseState(base, ps, builderUpdates);
|
|
10600
10793
|
await onProgress?.({
|
|
10601
10794
|
type: 'phase_complete',
|
|
10602
10795
|
path: node.ownerPath,
|
|
@@ -10622,16 +10815,18 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
10622
10815
|
// Could not read partial output — no state recovery
|
|
10623
10816
|
}
|
|
10624
10817
|
}
|
|
10625
|
-
return handlePhaseFailure('builder', err, executor, ps,
|
|
10626
|
-
metaPath: node.metaPath,
|
|
10627
|
-
current: currentMeta,
|
|
10628
|
-
structureHash,
|
|
10629
|
-
}, partialState);
|
|
10818
|
+
return handlePhaseFailure('builder', err, executor, ps, base, partialState);
|
|
10630
10819
|
}
|
|
10631
10820
|
}
|
|
10632
10821
|
// ── Critic executor ────────────────────────────────────────────────────
|
|
10633
10822
|
async function runCritic(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10634
10823
|
let ps = phaseRunning(phaseState, 'critic');
|
|
10824
|
+
const base = {
|
|
10825
|
+
metaPath: node.metaPath,
|
|
10826
|
+
current: currentMeta,
|
|
10827
|
+
config,
|
|
10828
|
+
structureHash,
|
|
10829
|
+
};
|
|
10635
10830
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10636
10831
|
// Build critic task using current meta's _content
|
|
10637
10832
|
const metaForCritic = { ...currentMeta };
|
|
@@ -10645,7 +10840,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
10645
10840
|
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
10646
10841
|
const result = await executor.spawn(criticTask, {
|
|
10647
10842
|
thinking: config.thinking,
|
|
10648
|
-
timeout: config.criticTimeout,
|
|
10843
|
+
timeout: currentMeta._criticTimeout ?? config.criticTimeout,
|
|
10649
10844
|
label: 'meta-critic',
|
|
10650
10845
|
});
|
|
10651
10846
|
const feedback = parseCriticOutput(result.output);
|
|
@@ -10655,6 +10850,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
10655
10850
|
const cycleComplete = isFullyFresh(ps);
|
|
10656
10851
|
const updates = {
|
|
10657
10852
|
_feedback: feedback,
|
|
10853
|
+
_critic: config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
10658
10854
|
_criticTokens: criticTokens,
|
|
10659
10855
|
_error: undefined,
|
|
10660
10856
|
};
|
|
@@ -10663,7 +10859,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
10663
10859
|
if (cycleComplete) {
|
|
10664
10860
|
updates._synthesisCount = (currentMeta._synthesisCount ?? 0) + 1;
|
|
10665
10861
|
}
|
|
10666
|
-
const updatedMeta = await persistPhaseState(
|
|
10862
|
+
const updatedMeta = await persistPhaseState(base, ps, updates);
|
|
10667
10863
|
// Archive on full-cycle only
|
|
10668
10864
|
if (cycleComplete) {
|
|
10669
10865
|
await createSnapshot(node.metaPath, updatedMeta);
|
|
@@ -10684,11 +10880,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
10684
10880
|
};
|
|
10685
10881
|
}
|
|
10686
10882
|
catch (err) {
|
|
10687
|
-
return handlePhaseFailure('critic', err, executor, ps,
|
|
10688
|
-
metaPath: node.metaPath,
|
|
10689
|
-
current: currentMeta,
|
|
10690
|
-
structureHash,
|
|
10691
|
-
});
|
|
10883
|
+
return handlePhaseFailure('critic', err, executor, ps, base);
|
|
10692
10884
|
}
|
|
10693
10885
|
}
|
|
10694
10886
|
|
|
@@ -10736,8 +10928,9 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
|
|
|
10736
10928
|
// Select best phase candidate
|
|
10737
10929
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
10738
10930
|
if (!winner) {
|
|
10739
|
-
//
|
|
10740
|
-
|
|
10931
|
+
// Tier 2 is now handled by the scheduler; orchestratePhase only handles
|
|
10932
|
+
// targeted (override) paths and Tier 1 corpus-wide selection.
|
|
10933
|
+
return { executed: false };
|
|
10741
10934
|
}
|
|
10742
10935
|
// Acquire lock
|
|
10743
10936
|
if (!acquireLock(winner.node.metaPath)) {
|
|
@@ -10803,48 +10996,6 @@ async function orchestrateTargeted(config, executor, watcher, targetPath, onProg
|
|
|
10803
10996
|
releaseLock(node.metaPath);
|
|
10804
10997
|
}
|
|
10805
10998
|
}
|
|
10806
|
-
/**
|
|
10807
|
-
* Tier 2 invalidation fallback: pick the stalest all-fresh meta,
|
|
10808
|
-
* run computeInvalidation (structure hash, steer, cross-refs), and
|
|
10809
|
-
* either execute the owed phase or bump _generatedAt.
|
|
10810
|
-
*/
|
|
10811
|
-
async function orchestrateTier2(candidates, config, executor, watcher, onProgress, logger) {
|
|
10812
|
-
const tier2 = selectTier2Candidate(candidates);
|
|
10813
|
-
if (!tier2)
|
|
10814
|
-
return { executed: false };
|
|
10815
|
-
if (!acquireLock(tier2.node.metaPath)) {
|
|
10816
|
-
logger?.debug({ path: tier2.node.metaPath }, 'Tier 2 candidate is locked, skipping');
|
|
10817
|
-
return { executed: false };
|
|
10818
|
-
}
|
|
10819
|
-
try {
|
|
10820
|
-
const currentMeta = await readMetaJson(tier2.node.metaPath);
|
|
10821
|
-
const { scopeFiles } = await getScopeFiles(tier2.node, watcher);
|
|
10822
|
-
const { phaseState, structureHash } = await computeInvalidation(currentMeta, scopeFiles, config, tier2.node);
|
|
10823
|
-
const owedPhase = getOwedPhase(phaseState);
|
|
10824
|
-
if (owedPhase) {
|
|
10825
|
-
// Something changed — persist invalidated state and execute owed phase
|
|
10826
|
-
await persistPhaseState({
|
|
10827
|
-
metaPath: tier2.node.metaPath,
|
|
10828
|
-
current: currentMeta,
|
|
10829
|
-
config,
|
|
10830
|
-
structureHash,
|
|
10831
|
-
}, phaseState, {});
|
|
10832
|
-
return await executePhase(tier2.node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
|
|
10833
|
-
}
|
|
10834
|
-
// Nothing changed — bump _generatedAt to delay re-checking
|
|
10835
|
-
await persistPhaseState({
|
|
10836
|
-
metaPath: tier2.node.metaPath,
|
|
10837
|
-
current: currentMeta,
|
|
10838
|
-
config,
|
|
10839
|
-
structureHash,
|
|
10840
|
-
}, phaseState, { _generatedAt: new Date().toISOString() });
|
|
10841
|
-
logger?.debug({ path: tier2.node.ownerPath }, 'Tier 2: no invalidation detected, bumped _generatedAt');
|
|
10842
|
-
return { executed: false };
|
|
10843
|
-
}
|
|
10844
|
-
finally {
|
|
10845
|
-
releaseLock(tier2.node.metaPath);
|
|
10846
|
-
}
|
|
10847
|
-
}
|
|
10848
10999
|
/**
|
|
10849
11000
|
* Execute exactly one phase on a meta.
|
|
10850
11001
|
*/
|
|
@@ -11306,12 +11457,6 @@ class WatcherHealthCheck {
|
|
|
11306
11457
|
return;
|
|
11307
11458
|
}
|
|
11308
11459
|
const data = (await res.json());
|
|
11309
|
-
// If rules were never successfully registered (startup failure),
|
|
11310
|
-
// attempt registration now that the watcher is reachable.
|
|
11311
|
-
if (!this.registrar.isRegistered) {
|
|
11312
|
-
this.logger.info('Rules not registered — attempting registration');
|
|
11313
|
-
await this.registrar.register();
|
|
11314
|
-
}
|
|
11315
11460
|
await this.registrar.checkAndReregister(data.uptime);
|
|
11316
11461
|
}
|
|
11317
11462
|
catch (err) {
|
|
@@ -11464,36 +11609,39 @@ class RuleRegistrar {
|
|
|
11464
11609
|
logger;
|
|
11465
11610
|
watcherClient;
|
|
11466
11611
|
lastWatcherUptime = null;
|
|
11467
|
-
|
|
11612
|
+
registering = false;
|
|
11468
11613
|
constructor(config, logger, watcher) {
|
|
11469
11614
|
this.config = config;
|
|
11470
11615
|
this.logger = logger;
|
|
11471
11616
|
this.watcherClient = watcher;
|
|
11472
11617
|
}
|
|
11473
|
-
/** Whether rules have been successfully registered. */
|
|
11474
|
-
get isRegistered() {
|
|
11475
|
-
return this.registered;
|
|
11476
|
-
}
|
|
11477
11618
|
/**
|
|
11478
11619
|
* Register rules with watcher. Retries with exponential backoff.
|
|
11479
11620
|
* Non-blocking — logs errors but never throws.
|
|
11480
11621
|
*/
|
|
11481
11622
|
async register() {
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11493
|
-
|
|
11623
|
+
if (this.registering)
|
|
11624
|
+
return;
|
|
11625
|
+
this.registering = true;
|
|
11626
|
+
try {
|
|
11627
|
+
const rules = buildMetaRules(this.config);
|
|
11628
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
11629
|
+
try {
|
|
11630
|
+
await this.watcherClient.registerRules(SOURCE, rules);
|
|
11631
|
+
this.logger.info('Virtual rules registered with watcher');
|
|
11632
|
+
return;
|
|
11633
|
+
}
|
|
11634
|
+
catch (err) {
|
|
11635
|
+
const delayMs = RETRY_BASE_MS * Math.pow(2, attempt);
|
|
11636
|
+
this.logger.warn({ attempt: attempt + 1, delayMs, err }, 'Rule registration failed, retrying');
|
|
11637
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
11638
|
+
}
|
|
11494
11639
|
}
|
|
11640
|
+
this.logger.error('Rule registration failed after max retries — service degraded');
|
|
11641
|
+
}
|
|
11642
|
+
finally {
|
|
11643
|
+
this.registering = false;
|
|
11495
11644
|
}
|
|
11496
|
-
this.logger.error('Rule registration failed after max retries — service degraded');
|
|
11497
11645
|
}
|
|
11498
11646
|
/**
|
|
11499
11647
|
* Check watcher uptime and re-register if it decreased (restart detected).
|
|
@@ -11504,7 +11652,6 @@ class RuleRegistrar {
|
|
|
11504
11652
|
if (this.lastWatcherUptime !== null &&
|
|
11505
11653
|
currentUptime < this.lastWatcherUptime) {
|
|
11506
11654
|
this.logger.info({ previous: this.lastWatcherUptime, current: currentUptime }, 'Watcher restart detected — re-registering rules');
|
|
11507
|
-
this.registered = false;
|
|
11508
11655
|
await this.register();
|
|
11509
11656
|
}
|
|
11510
11657
|
this.lastWatcherUptime = currentUptime;
|
|
@@ -11574,6 +11721,12 @@ async function createMeta(ownerPath, options) {
|
|
|
11574
11721
|
metaJson._crossRefs = options.crossRefs;
|
|
11575
11722
|
if (options?.steer !== undefined)
|
|
11576
11723
|
metaJson._steer = options.steer;
|
|
11724
|
+
if (options?.architectTimeout !== undefined)
|
|
11725
|
+
metaJson._architectTimeout = options.architectTimeout;
|
|
11726
|
+
if (options?.builderTimeout !== undefined)
|
|
11727
|
+
metaJson._builderTimeout = options.builderTimeout;
|
|
11728
|
+
if (options?.criticTimeout !== undefined)
|
|
11729
|
+
metaJson._criticTimeout = options.criticTimeout;
|
|
11577
11730
|
const metaJsonPath = join(metaDir, 'meta.json');
|
|
11578
11731
|
await writeFile(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
11579
11732
|
return { metaDir, _id };
|
|
@@ -11602,15 +11755,24 @@ function metaExists(ownerPath) {
|
|
|
11602
11755
|
/**
|
|
11603
11756
|
* Extract parent directory paths from watcher walk results.
|
|
11604
11757
|
*
|
|
11605
|
-
* Walk returns file paths; we need the unique set of
|
|
11606
|
-
*
|
|
11758
|
+
* Walk returns file paths; we need the unique set of parent directories that
|
|
11759
|
+
* could be owners. When {@link parentDepth} is specified, walk up that many
|
|
11760
|
+
* additional levels from each file's immediate parent. The walk is clamped at
|
|
11761
|
+
* the filesystem root to prevent escaping the watched scope.
|
|
11607
11762
|
*/
|
|
11608
|
-
function extractDirectories(filePaths, logger) {
|
|
11763
|
+
function extractDirectories(filePaths, parentDepth = 0, logger) {
|
|
11609
11764
|
const dirs = new Set();
|
|
11610
11765
|
for (const fp of filePaths) {
|
|
11611
11766
|
// Normalize backslash paths (Windows) to forward slashes before posix.dirname
|
|
11612
11767
|
const normalized = normalizePath(fp);
|
|
11613
|
-
|
|
11768
|
+
let dir = posix.dirname(normalized);
|
|
11769
|
+
// Walk up parentDepth additional levels, clamping at filesystem root
|
|
11770
|
+
for (let i = 0; i < parentDepth; i++) {
|
|
11771
|
+
const parent = posix.dirname(dir);
|
|
11772
|
+
if (parent === dir)
|
|
11773
|
+
break; // reached root
|
|
11774
|
+
dir = parent;
|
|
11775
|
+
}
|
|
11614
11776
|
if (dir !== '.' && dir !== '/') {
|
|
11615
11777
|
dirs.add(dir);
|
|
11616
11778
|
}
|
|
@@ -11635,11 +11797,14 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
11635
11797
|
const candidates = new Map();
|
|
11636
11798
|
for (const rule of rules) {
|
|
11637
11799
|
const files = await watcher.walk([rule.match]);
|
|
11638
|
-
const dirs = extractDirectories(files, logger);
|
|
11800
|
+
const dirs = extractDirectories(files, rule.parentDepth, logger);
|
|
11639
11801
|
for (const dir of dirs) {
|
|
11640
11802
|
candidates.set(dir, {
|
|
11641
11803
|
steer: rule.steer,
|
|
11642
11804
|
crossRefs: rule.crossRefs,
|
|
11805
|
+
architectTimeout: rule.architectTimeout,
|
|
11806
|
+
builderTimeout: rule.builderTimeout,
|
|
11807
|
+
criticTimeout: rule.criticTimeout,
|
|
11643
11808
|
});
|
|
11644
11809
|
}
|
|
11645
11810
|
}
|
|
@@ -11654,10 +11819,7 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
11654
11819
|
const seededPaths = [];
|
|
11655
11820
|
for (const candidate of toSeed) {
|
|
11656
11821
|
try {
|
|
11657
|
-
await createMeta(candidate.path,
|
|
11658
|
-
steer: candidate.steer,
|
|
11659
|
-
crossRefs: candidate.crossRefs,
|
|
11660
|
-
});
|
|
11822
|
+
await createMeta(candidate.path, candidate);
|
|
11661
11823
|
seededPaths.push(candidate.path);
|
|
11662
11824
|
logger?.info({ path: candidate.path }, 'auto-seeded meta');
|
|
11663
11825
|
}
|
|
@@ -11822,7 +11984,7 @@ class Scheduler {
|
|
|
11822
11984
|
const candidates = buildPhaseCandidates(result.entries, this.config.architectEvery);
|
|
11823
11985
|
const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
|
|
11824
11986
|
if (!winner)
|
|
11825
|
-
return
|
|
11987
|
+
return await this.discoverTier2Phase(candidates);
|
|
11826
11988
|
return {
|
|
11827
11989
|
path: winner.node.metaPath,
|
|
11828
11990
|
phase: winner.owedPhase,
|
|
@@ -11834,6 +11996,59 @@ class Scheduler {
|
|
|
11834
11996
|
return null;
|
|
11835
11997
|
}
|
|
11836
11998
|
}
|
|
11999
|
+
/**
|
|
12000
|
+
* Tier 2 invalidation: iterate all-fresh candidates (stalest first),
|
|
12001
|
+
* run computeInvalidation, and return the first that produces an owed phase.
|
|
12002
|
+
*/
|
|
12003
|
+
async discoverTier2Phase(candidates) {
|
|
12004
|
+
const allTier2 = selectAllTier2Candidates(candidates);
|
|
12005
|
+
const limit = this.config.tier2ScanLimit;
|
|
12006
|
+
const tier2Candidates = allTier2.slice(0, limit);
|
|
12007
|
+
if (allTier2.length > limit) {
|
|
12008
|
+
this.logger.debug({ total: allTier2.length, limit }, 'Tier 2 scan limit reached, scanning subset');
|
|
12009
|
+
}
|
|
12010
|
+
let dirty = false;
|
|
12011
|
+
for (const t2 of tier2Candidates) {
|
|
12012
|
+
if (!acquireLock(t2.node.metaPath))
|
|
12013
|
+
continue;
|
|
12014
|
+
try {
|
|
12015
|
+
const currentMeta = await readMetaJson(t2.node.metaPath);
|
|
12016
|
+
const { scopeFiles } = await getScopeFiles(t2.node, this.watcher);
|
|
12017
|
+
const result = await computeInvalidation(currentMeta, scopeFiles, this.config, t2.node);
|
|
12018
|
+
const owedPhase = getOwedPhase(result.phaseState);
|
|
12019
|
+
if (owedPhase) {
|
|
12020
|
+
await persistPhaseState({
|
|
12021
|
+
metaPath: t2.node.metaPath,
|
|
12022
|
+
current: currentMeta,
|
|
12023
|
+
config: this.config,
|
|
12024
|
+
structureHash: result.inputStatus.structureHash,
|
|
12025
|
+
}, result.phaseState, {});
|
|
12026
|
+
this.cache.invalidate();
|
|
12027
|
+
return {
|
|
12028
|
+
path: t2.node.metaPath,
|
|
12029
|
+
phase: owedPhase,
|
|
12030
|
+
band: getPriorityBand(result.phaseState),
|
|
12031
|
+
};
|
|
12032
|
+
}
|
|
12033
|
+
// No invalidation — bump _generatedAt to delay re-checking
|
|
12034
|
+
await persistPhaseState({
|
|
12035
|
+
metaPath: t2.node.metaPath,
|
|
12036
|
+
current: currentMeta,
|
|
12037
|
+
config: this.config,
|
|
12038
|
+
structureHash: result.inputStatus.structureHash,
|
|
12039
|
+
}, result.phaseState, {
|
|
12040
|
+
_generatedAt: new Date().toISOString(),
|
|
12041
|
+
});
|
|
12042
|
+
dirty = true;
|
|
12043
|
+
}
|
|
12044
|
+
finally {
|
|
12045
|
+
releaseLock(t2.node.metaPath);
|
|
12046
|
+
}
|
|
12047
|
+
}
|
|
12048
|
+
if (dirty)
|
|
12049
|
+
this.cache.invalidate();
|
|
12050
|
+
return null;
|
|
12051
|
+
}
|
|
11837
12052
|
}
|
|
11838
12053
|
|
|
11839
12054
|
/**
|
|
@@ -11853,7 +12068,7 @@ function sanitizeConfig(config) {
|
|
|
11853
12068
|
}
|
|
11854
12069
|
function registerConfigRoute(app, deps) {
|
|
11855
12070
|
const configHandler = createConfigQueryHandler(() => sanitizeConfig(deps.config));
|
|
11856
|
-
app.get('
|
|
12071
|
+
app.get(getEndpoint('config').path, async (request, reply) => {
|
|
11857
12072
|
const { path } = request.query;
|
|
11858
12073
|
const result = await configHandler({ path });
|
|
11859
12074
|
return reply.status(result.status).send(result.body);
|
|
@@ -11872,7 +12087,7 @@ function registerConfigRoute(app, deps) {
|
|
|
11872
12087
|
*/
|
|
11873
12088
|
/** Register the POST /config/apply route. */
|
|
11874
12089
|
function registerConfigApplyRoute(app, configPath) {
|
|
11875
|
-
app.post('
|
|
12090
|
+
app.post(getEndpoint('configApply').path, async (request, reply) => {
|
|
11876
12091
|
if (!configPath) {
|
|
11877
12092
|
return reply
|
|
11878
12093
|
.status(500)
|
|
@@ -11961,42 +12176,32 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
11961
12176
|
*
|
|
11962
12177
|
* @module routes/metas
|
|
11963
12178
|
*/
|
|
12179
|
+
/** Reusable Zod schema for boolean query string parameters ('true'/'false'). */
|
|
12180
|
+
const boolQueryParam = z.enum(['true', 'false']).transform((v) => v === 'true');
|
|
11964
12181
|
const metasQuerySchema = z.object({
|
|
11965
12182
|
pathPrefix: z.string().optional(),
|
|
11966
|
-
hasError:
|
|
11967
|
-
.enum(['true', 'false'])
|
|
11968
|
-
.transform((v) => v === 'true')
|
|
11969
|
-
.optional(),
|
|
12183
|
+
hasError: boolQueryParam.optional(),
|
|
11970
12184
|
staleHours: z
|
|
11971
12185
|
.string()
|
|
11972
12186
|
.transform(Number)
|
|
11973
12187
|
.pipe(z.number().positive())
|
|
11974
12188
|
.optional(),
|
|
11975
|
-
neverSynthesized:
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
.optional(),
|
|
11979
|
-
locked: z
|
|
11980
|
-
.enum(['true', 'false'])
|
|
11981
|
-
.transform((v) => v === 'true')
|
|
11982
|
-
.optional(),
|
|
11983
|
-
disabled: z
|
|
11984
|
-
.enum(['true', 'false'])
|
|
11985
|
-
.transform((v) => v === 'true')
|
|
11986
|
-
.optional(),
|
|
12189
|
+
neverSynthesized: boolQueryParam.optional(),
|
|
12190
|
+
locked: boolQueryParam.optional(),
|
|
12191
|
+
disabled: boolQueryParam.optional(),
|
|
11987
12192
|
fields: z.string().optional(),
|
|
11988
12193
|
});
|
|
11989
12194
|
const metaDetailQuerySchema = z.object({
|
|
11990
12195
|
fields: z.string().optional(),
|
|
11991
12196
|
includeArchive: z
|
|
11992
12197
|
.union([
|
|
11993
|
-
|
|
12198
|
+
boolQueryParam,
|
|
11994
12199
|
z.string().transform(Number).pipe(z.number().int().nonnegative()),
|
|
11995
12200
|
])
|
|
11996
12201
|
.optional(),
|
|
11997
12202
|
});
|
|
11998
12203
|
function registerMetasRoutes(app, deps) {
|
|
11999
|
-
app.get('
|
|
12204
|
+
app.get(getEndpoint('listMetas').path, async (request) => {
|
|
12000
12205
|
const query = metasQuerySchema.parse(request.query);
|
|
12001
12206
|
const { config, watcher } = deps;
|
|
12002
12207
|
const result = await listMetas(config, watcher);
|
|
@@ -12068,7 +12273,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
12068
12273
|
});
|
|
12069
12274
|
return { summary, metas };
|
|
12070
12275
|
});
|
|
12071
|
-
app.get('
|
|
12276
|
+
app.get(getEndpoint('metaDetail').path, async (request, reply) => {
|
|
12072
12277
|
const query = metaDetailQuerySchema.parse(request.query);
|
|
12073
12278
|
const { config, watcher } = deps;
|
|
12074
12279
|
const targetPath = normalizePath(decodeURIComponent(request.params.path));
|
|
@@ -12162,7 +12367,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
12162
12367
|
/**
|
|
12163
12368
|
* PATCH /metas/:path — update user-settable reserved properties on a meta.
|
|
12164
12369
|
*
|
|
12165
|
-
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled.
|
|
12370
|
+
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled, _architectTimeout, _builderTimeout, _criticTimeout.
|
|
12166
12371
|
* Set a field to null to remove it. Unknown keys are rejected.
|
|
12167
12372
|
*
|
|
12168
12373
|
* @module routes/metasUpdate
|
|
@@ -12174,10 +12379,13 @@ const updateBodySchema = z
|
|
|
12174
12379
|
_depth: z.union([z.number(), z.null()]).optional(),
|
|
12175
12380
|
_crossRefs: z.union([z.array(z.string()), z.null()]).optional(),
|
|
12176
12381
|
_disabled: z.union([z.boolean(), z.null()]).optional(),
|
|
12382
|
+
_architectTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
12383
|
+
_builderTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
12384
|
+
_criticTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
12177
12385
|
})
|
|
12178
12386
|
.strict();
|
|
12179
12387
|
function registerMetasUpdateRoute(app, deps) {
|
|
12180
|
-
app.patch('
|
|
12388
|
+
app.patch(getEndpoint('updateMeta').path, async (request, reply) => {
|
|
12181
12389
|
const parseResult = updateBodySchema.safeParse(request.body);
|
|
12182
12390
|
if (!parseResult.success) {
|
|
12183
12391
|
return reply.status(400).send({
|
|
@@ -12199,13 +12407,7 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12199
12407
|
});
|
|
12200
12408
|
}
|
|
12201
12409
|
const metaJsonPath = join(metaDir, 'meta.json');
|
|
12202
|
-
const KEYS =
|
|
12203
|
-
'_steer',
|
|
12204
|
-
'_emphasis',
|
|
12205
|
-
'_depth',
|
|
12206
|
-
'_crossRefs',
|
|
12207
|
-
'_disabled',
|
|
12208
|
-
];
|
|
12410
|
+
const KEYS = Object.keys(updateBodySchema.shape);
|
|
12209
12411
|
const toDelete = new Set();
|
|
12210
12412
|
const toSet = {};
|
|
12211
12413
|
for (const key of KEYS) {
|
|
@@ -12243,7 +12445,7 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12243
12445
|
* @module routes/preview
|
|
12244
12446
|
*/
|
|
12245
12447
|
function registerPreviewRoute(app, deps) {
|
|
12246
|
-
app.get('
|
|
12448
|
+
app.get(getEndpoint('preview').path, async (request, reply) => {
|
|
12247
12449
|
const { config, watcher, cache } = deps;
|
|
12248
12450
|
const query = request.query;
|
|
12249
12451
|
let result;
|
|
@@ -12281,12 +12483,8 @@ function registerPreviewRoute(app, deps) {
|
|
|
12281
12483
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
12282
12484
|
// Compute invalidation inputs (DRY: reuse phaseState/invalidate logic)
|
|
12283
12485
|
const invalidation = await computeInvalidation(meta, scopeFiles, config, targetNode);
|
|
12284
|
-
const { architectInvalidators,
|
|
12285
|
-
const
|
|
12286
|
-
const structureChanged = structureHash !== meta._structureHash;
|
|
12287
|
-
const { steerChanged } = invalidation;
|
|
12288
|
-
const { architectChanged, crossRefsDeclChanged } = stalenessInputs;
|
|
12289
|
-
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
12486
|
+
const { architectInvalidators, inputStatus, phaseState } = invalidation;
|
|
12487
|
+
const architectTriggered = architectInvalidators.length > 0;
|
|
12290
12488
|
// Delta files
|
|
12291
12489
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
12292
12490
|
// EMA token estimates
|
|
@@ -12300,14 +12498,6 @@ function registerPreviewRoute(app, deps) {
|
|
|
12300
12498
|
? Math.round((Date.now() - new Date(meta._generatedAt).getTime()) / 1000)
|
|
12301
12499
|
: null;
|
|
12302
12500
|
const stalenessScore = computeStalenessScore(stalenessSeconds, meta._depth ?? 0, meta._emphasis ?? 1, config.depthWeight);
|
|
12303
|
-
// Phase state
|
|
12304
|
-
const phaseState = derivePhaseState(meta, {
|
|
12305
|
-
structureChanged,
|
|
12306
|
-
steerChanged,
|
|
12307
|
-
architectChanged,
|
|
12308
|
-
crossRefsChanged: crossRefsDeclChanged,
|
|
12309
|
-
architectEvery: config.architectEvery,
|
|
12310
|
-
});
|
|
12311
12501
|
const owedPhase = getOwedPhase(phaseState);
|
|
12312
12502
|
const priorityBand = getPriorityBand(phaseState);
|
|
12313
12503
|
return {
|
|
@@ -12318,10 +12508,17 @@ function registerPreviewRoute(app, deps) {
|
|
|
12318
12508
|
},
|
|
12319
12509
|
architectWillRun: architectTriggered,
|
|
12320
12510
|
architectReason: [
|
|
12321
|
-
...(
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
...((
|
|
12511
|
+
...(architectInvalidators.includes('firstRun')
|
|
12512
|
+
? ['no cached builder (first run)']
|
|
12513
|
+
: []),
|
|
12514
|
+
...(architectInvalidators.includes('structureHash')
|
|
12515
|
+
? ['structure changed']
|
|
12516
|
+
: []),
|
|
12517
|
+
...(architectInvalidators.includes('steer') ? ['steer changed'] : []),
|
|
12518
|
+
...(architectInvalidators.includes('_crossRefs')
|
|
12519
|
+
? ['cross-refs changed']
|
|
12520
|
+
: []),
|
|
12521
|
+
...(architectInvalidators.includes('architectEvery')
|
|
12325
12522
|
? ['periodic refresh']
|
|
12326
12523
|
: []),
|
|
12327
12524
|
].join(', ') || 'not triggered',
|
|
@@ -12338,7 +12535,7 @@ function registerPreviewRoute(app, deps) {
|
|
|
12338
12535
|
owedPhase,
|
|
12339
12536
|
priorityBand,
|
|
12340
12537
|
phaseState,
|
|
12341
|
-
|
|
12538
|
+
inputStatus,
|
|
12342
12539
|
architectInvalidators,
|
|
12343
12540
|
};
|
|
12344
12541
|
});
|
|
@@ -12356,7 +12553,7 @@ function registerPreviewRoute(app, deps) {
|
|
|
12356
12553
|
/** Register queue management routes. */
|
|
12357
12554
|
function registerQueueRoutes(app, deps) {
|
|
12358
12555
|
const { queue } = deps;
|
|
12359
|
-
app.get('
|
|
12556
|
+
app.get(getEndpoint('queue').path, async () => {
|
|
12360
12557
|
const currentPhase = queue.currentPhase;
|
|
12361
12558
|
const overrides = queue.overrides;
|
|
12362
12559
|
// Compute owedPhase for each override entry by reading meta state
|
|
@@ -12432,11 +12629,11 @@ function registerQueueRoutes(app, deps) {
|
|
|
12432
12629
|
state: queue.getState(),
|
|
12433
12630
|
};
|
|
12434
12631
|
});
|
|
12435
|
-
app.post('
|
|
12632
|
+
app.post(getEndpoint('queueClear').path, () => {
|
|
12436
12633
|
const removed = queue.clearOverrides();
|
|
12437
12634
|
return { cleared: removed };
|
|
12438
12635
|
});
|
|
12439
|
-
app.post('
|
|
12636
|
+
app.post(getEndpoint('abort').path, async (_request, reply) => {
|
|
12440
12637
|
// Check 3-layer current first
|
|
12441
12638
|
const currentPhase = queue.currentPhase;
|
|
12442
12639
|
const current = currentPhase ?? queue.current;
|
|
@@ -12498,7 +12695,7 @@ const seedBodySchema = z.object({
|
|
|
12498
12695
|
steer: z.string().optional(),
|
|
12499
12696
|
});
|
|
12500
12697
|
function registerSeedRoute(app, deps) {
|
|
12501
|
-
app.post('
|
|
12698
|
+
app.post(getEndpoint('seed').path, async (request, reply) => {
|
|
12502
12699
|
const body = seedBodySchema.parse(request.body);
|
|
12503
12700
|
if (metaExists(body.path)) {
|
|
12504
12701
|
return reply.status(409).send({
|
|
@@ -12635,7 +12832,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12635
12832
|
dependencies: {
|
|
12636
12833
|
watcher: {
|
|
12637
12834
|
...watcherHealth,
|
|
12638
|
-
rulesRegistered:
|
|
12835
|
+
rulesRegistered: true,
|
|
12639
12836
|
},
|
|
12640
12837
|
gateway: gatewayHealth,
|
|
12641
12838
|
},
|
|
@@ -12644,7 +12841,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12644
12841
|
};
|
|
12645
12842
|
},
|
|
12646
12843
|
});
|
|
12647
|
-
app.get('
|
|
12844
|
+
app.get(getEndpoint('status').path, async (_request, reply) => {
|
|
12648
12845
|
const result = await statusHandler();
|
|
12649
12846
|
return reply.status(result.status).send(result.body);
|
|
12650
12847
|
});
|
|
@@ -12663,24 +12860,41 @@ const synthesizeBodySchema = z.object({
|
|
|
12663
12860
|
});
|
|
12664
12861
|
/** Register the POST /synthesize route. */
|
|
12665
12862
|
function registerSynthesizeRoute(app, deps) {
|
|
12666
|
-
app.post('
|
|
12863
|
+
app.post(getEndpoint('synthesize').path, async (request, reply) => {
|
|
12667
12864
|
const body = synthesizeBodySchema.parse(request.body);
|
|
12668
12865
|
const { config, watcher, queue, cache } = deps;
|
|
12669
12866
|
if (body.path) {
|
|
12670
12867
|
// Path-targeted trigger: create override entry
|
|
12671
12868
|
const targetPath = resolveMetaDir(body.path);
|
|
12672
|
-
// Read meta
|
|
12869
|
+
// Read meta and recompute invalidation against current inputs
|
|
12870
|
+
// (structure hash, steer, cross-refs, prompt snapshots) rather than
|
|
12871
|
+
// trusting the cached _phaseState. Fixes #160.
|
|
12673
12872
|
let owedPhase = null;
|
|
12674
12873
|
let meta;
|
|
12675
12874
|
try {
|
|
12676
12875
|
meta = await readMetaJson(targetPath);
|
|
12677
|
-
const
|
|
12678
|
-
|
|
12876
|
+
const node = await buildMinimalNode(normalizePath(targetPath), watcher);
|
|
12877
|
+
const { scopeFiles } = await getScopeFiles(node, watcher);
|
|
12878
|
+
const invalidation = await computeInvalidation(meta, scopeFiles, config, node);
|
|
12879
|
+
owedPhase = getOwedPhase(invalidation.phaseState);
|
|
12880
|
+
// Persist recomputed phase state + structure hash when stale.
|
|
12881
|
+
// Matches the scheduler's Tier 2 pattern: always persist so the
|
|
12882
|
+
// stored _phaseState reflects reality for subsequent reads.
|
|
12883
|
+
if (owedPhase) {
|
|
12884
|
+
await persistPhaseState({
|
|
12885
|
+
metaPath: targetPath,
|
|
12886
|
+
current: meta,
|
|
12887
|
+
config,
|
|
12888
|
+
structureHash: invalidation.inputStatus.structureHash,
|
|
12889
|
+
}, invalidation.phaseState, {});
|
|
12890
|
+
cache.invalidate();
|
|
12891
|
+
}
|
|
12679
12892
|
}
|
|
12680
12893
|
catch {
|
|
12681
|
-
// Meta unreadable — proceed,
|
|
12894
|
+
// Meta unreadable or watcher unavailable — proceed,
|
|
12895
|
+
// phase will be evaluated at dequeue time
|
|
12682
12896
|
}
|
|
12683
|
-
// Fully fresh meta → skip
|
|
12897
|
+
// Fully fresh meta → skip
|
|
12684
12898
|
if (owedPhase === null && meta && (meta._phaseState || meta._content)) {
|
|
12685
12899
|
return await reply.code(200).send({
|
|
12686
12900
|
status: 'skipped',
|
|
@@ -12738,7 +12952,7 @@ const unlockBodySchema = z.object({
|
|
|
12738
12952
|
path: z.string().min(1),
|
|
12739
12953
|
});
|
|
12740
12954
|
function registerUnlockRoute(app, deps) {
|
|
12741
|
-
app.post('
|
|
12955
|
+
app.post(getEndpoint('unlock').path, (request, reply) => {
|
|
12742
12956
|
const body = unlockBodySchema.parse(request.body);
|
|
12743
12957
|
const metaDir = resolveMetaDir(body.path);
|
|
12744
12958
|
const lockPath = join(metaDir, '.lock');
|
|
@@ -13135,9 +13349,7 @@ async function startService(config, configPath) {
|
|
|
13135
13349
|
routeDeps.registrar = registrar;
|
|
13136
13350
|
void registrar.register().then(() => {
|
|
13137
13351
|
routeDeps.ready = true;
|
|
13138
|
-
|
|
13139
|
-
void verifyRuleApplication(watcher, logger);
|
|
13140
|
-
}
|
|
13352
|
+
void verifyRuleApplication(watcher, logger);
|
|
13141
13353
|
}, () => {
|
|
13142
13354
|
// Registration failed after max retries — mark ready anyway
|
|
13143
13355
|
routeDeps.ready = true;
|
|
@@ -13330,6 +13542,17 @@ const metaJsonSchema = z
|
|
|
13330
13542
|
_error: metaErrorSchema.optional(),
|
|
13331
13543
|
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
13332
13544
|
_disabled: z.boolean().optional(),
|
|
13545
|
+
/** Per-entity timeout override for the architect phase (seconds, min 30). */
|
|
13546
|
+
_architectTimeout: z.number().int().min(30).optional(),
|
|
13547
|
+
/** Per-entity timeout override for the builder phase (seconds, min 30). */
|
|
13548
|
+
_builderTimeout: z.number().int().min(30).optional(),
|
|
13549
|
+
/** Per-entity timeout override for the critic phase (seconds, min 30). */
|
|
13550
|
+
_criticTimeout: z.number().int().min(30).optional(),
|
|
13551
|
+
/**
|
|
13552
|
+
* SHA-256 hash of ancestor _builder text at last synthesis.
|
|
13553
|
+
* Observability only — no invalidation cascade.
|
|
13554
|
+
*/
|
|
13555
|
+
_ancestorBuilderHash: z.string().optional(),
|
|
13333
13556
|
/**
|
|
13334
13557
|
* Per-phase state machine record. Engine-managed.
|
|
13335
13558
|
* Keyed by phase name (architect, builder, critic) with status values.
|