@karmaniverous/jeeves-meta 0.15.8 → 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 +405 -200
- package/dist/discovery/getAncestorMeta.d.ts +16 -0
- package/dist/discovery/index.d.ts +1 -0
- package/dist/index.js +402 -185
- package/dist/interfaces/MetaContext.d.ts +2 -0
- package/dist/phaseState/derivePhaseState.d.ts +0 -2
- package/dist/phaseState/index.d.ts +1 -1
- package/dist/phaseState/invalidate.d.ts +5 -7
- 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/schema/config.d.ts +8 -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({
|
|
@@ -9324,7 +9478,7 @@ class GatewayExecutor {
|
|
|
9324
9478
|
'Write your complete output to a file using the Write tool at:\n' +
|
|
9325
9479
|
outputPath +
|
|
9326
9480
|
'\n\n' +
|
|
9327
|
-
'After writing the file,
|
|
9481
|
+
'After writing the file, your final message must be exactly: ANNOUNCE_SKIP';
|
|
9328
9482
|
// Step 1: Spawn the sub-agent session (unique label per cycle to avoid
|
|
9329
9483
|
// "label already in use" errors — gateway labels persist after session completion)
|
|
9330
9484
|
const labelBase = options?.label ?? 'jeeves-meta-synthesis';
|
|
@@ -9392,7 +9546,9 @@ class GatewayExecutor {
|
|
|
9392
9546
|
}
|
|
9393
9547
|
}
|
|
9394
9548
|
}
|
|
9395
|
-
// 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).
|
|
9396
9552
|
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9397
9553
|
const msg = msgArray[i];
|
|
9398
9554
|
if (msg.role === 'assistant' && msg.content) {
|
|
@@ -9404,7 +9560,7 @@ class GatewayExecutor {
|
|
|
9404
9560
|
.map((b) => b.text)
|
|
9405
9561
|
.join('\n')
|
|
9406
9562
|
: '';
|
|
9407
|
-
if (text)
|
|
9563
|
+
if (text && text.trim() !== 'ANNOUNCE_SKIP')
|
|
9408
9564
|
return { output: text, tokens };
|
|
9409
9565
|
}
|
|
9410
9566
|
}
|
|
@@ -9434,11 +9590,16 @@ class GatewayExecutor {
|
|
|
9434
9590
|
function createLogger(config) {
|
|
9435
9591
|
const level = config?.level ?? 'info';
|
|
9436
9592
|
if (config?.file) {
|
|
9437
|
-
const
|
|
9438
|
-
|
|
9439
|
-
|
|
9593
|
+
const fileStream = pino.destination({
|
|
9594
|
+
dest: config.file,
|
|
9595
|
+
sync: false,
|
|
9596
|
+
mkdir: true,
|
|
9440
9597
|
});
|
|
9441
|
-
|
|
9598
|
+
const multistream = pino.multistream([
|
|
9599
|
+
{ stream: process.stdout },
|
|
9600
|
+
{ stream: fileStream },
|
|
9601
|
+
]);
|
|
9602
|
+
return pino({ level }, multistream);
|
|
9442
9603
|
}
|
|
9443
9604
|
return pino({ level });
|
|
9444
9605
|
}
|
|
@@ -9560,6 +9721,21 @@ async function buildContextPackage(node, meta, watcher, logger) {
|
|
|
9560
9721
|
}
|
|
9561
9722
|
// Archive paths
|
|
9562
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
|
+
}
|
|
9563
9739
|
return {
|
|
9564
9740
|
path: node.metaPath,
|
|
9565
9741
|
scopeFiles,
|
|
@@ -9571,6 +9747,7 @@ async function buildContextPackage(node, meta, watcher, logger) {
|
|
|
9571
9747
|
steer: meta._steer ?? null,
|
|
9572
9748
|
previousState: meta._state ?? null,
|
|
9573
9749
|
archives,
|
|
9750
|
+
ancestorBuilder,
|
|
9574
9751
|
};
|
|
9575
9752
|
}
|
|
9576
9753
|
|
|
@@ -9618,6 +9795,12 @@ function appendMetaSections(sections, heading, metas) {
|
|
|
9618
9795
|
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
9619
9796
|
}
|
|
9620
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
|
+
}
|
|
9621
9804
|
/** Append optional context sections shared across all step prompts. */
|
|
9622
9805
|
function appendSharedSections(sections, ctx, options) {
|
|
9623
9806
|
const opts = {
|
|
@@ -9657,7 +9840,7 @@ function buildArchitectTask(ctx, meta, config) {
|
|
|
9657
9840
|
const sections = [
|
|
9658
9841
|
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
9659
9842
|
'',
|
|
9660
|
-
|
|
9843
|
+
config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
|
|
9661
9844
|
'',
|
|
9662
9845
|
'## SCOPE',
|
|
9663
9846
|
`Path: ${ctx.path}`,
|
|
@@ -9667,6 +9850,7 @@ function buildArchitectTask(ctx, meta, config) {
|
|
|
9667
9850
|
'### File listing (scope)',
|
|
9668
9851
|
condenseScopeFiles(ctx.scopeFiles),
|
|
9669
9852
|
];
|
|
9853
|
+
appendAncestorContext(sections, ctx);
|
|
9670
9854
|
// Inject previous _builder so architect can see its own prior output
|
|
9671
9855
|
if (meta._builder) {
|
|
9672
9856
|
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
@@ -9697,6 +9881,7 @@ function buildBuilderTask(ctx, meta, config) {
|
|
|
9697
9881
|
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
9698
9882
|
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
9699
9883
|
];
|
|
9884
|
+
appendAncestorContext(sections, ctx);
|
|
9700
9885
|
if (ctx.previousState != null) {
|
|
9701
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), '```');
|
|
9702
9887
|
}
|
|
@@ -9719,7 +9904,7 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
9719
9904
|
const sections = [
|
|
9720
9905
|
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
9721
9906
|
'',
|
|
9722
|
-
|
|
9907
|
+
config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
9723
9908
|
'',
|
|
9724
9909
|
'## SYNTHESIS TO EVALUATE',
|
|
9725
9910
|
meta._content ?? '(No content produced)',
|
|
@@ -9853,7 +10038,7 @@ function enforceInvariant(state) {
|
|
|
9853
10038
|
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
9854
10039
|
/**
|
|
9855
10040
|
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
9856
|
-
* Triggers:
|
|
10041
|
+
* Triggers: first run, _structureHash change, _steer change,
|
|
9857
10042
|
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
9858
10043
|
*/
|
|
9859
10044
|
function invalidateArchitect(state) {
|
|
@@ -10051,31 +10236,6 @@ function derivePhaseState(meta, inputs) {
|
|
|
10051
10236
|
if (!meta._content && !meta._builder) {
|
|
10052
10237
|
return initialPhaseState();
|
|
10053
10238
|
}
|
|
10054
|
-
// Check architect invalidation (when inputs are provided)
|
|
10055
|
-
if (inputs) {
|
|
10056
|
-
// Progressive metas: structure changes invalidate builder, not architect
|
|
10057
|
-
const structureInvalidatesArchitect = inputs.structureChanged && meta._state === undefined;
|
|
10058
|
-
const architectInvalidated = structureInvalidatesArchitect ||
|
|
10059
|
-
inputs.steerChanged ||
|
|
10060
|
-
inputs.architectChanged ||
|
|
10061
|
-
inputs.crossRefsChanged ||
|
|
10062
|
-
(meta._synthesisCount ?? 0) >= inputs.architectEvery;
|
|
10063
|
-
if (architectInvalidated) {
|
|
10064
|
-
return {
|
|
10065
|
-
architect: 'pending',
|
|
10066
|
-
builder: 'stale',
|
|
10067
|
-
critic: 'stale',
|
|
10068
|
-
};
|
|
10069
|
-
}
|
|
10070
|
-
// Progressive meta with structure change: builder-only invalidation
|
|
10071
|
-
if (inputs.structureChanged && meta._state !== undefined) {
|
|
10072
|
-
return {
|
|
10073
|
-
architect: 'fresh',
|
|
10074
|
-
builder: 'pending',
|
|
10075
|
-
critic: 'stale',
|
|
10076
|
-
};
|
|
10077
|
-
}
|
|
10078
|
-
}
|
|
10079
10239
|
// Has _builder but no _content: builder is pending
|
|
10080
10240
|
if (meta._builder && !meta._content) {
|
|
10081
10241
|
return {
|
|
@@ -10124,6 +10284,14 @@ function computeStructureHash(filePaths) {
|
|
|
10124
10284
|
*
|
|
10125
10285
|
* @module phaseState/invalidate
|
|
10126
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
|
+
}
|
|
10127
10295
|
/**
|
|
10128
10296
|
* Compute invalidation inputs and apply cascade for a single meta.
|
|
10129
10297
|
*
|
|
@@ -10146,10 +10314,16 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
|
|
|
10146
10314
|
const structureChanged = structureHash !== meta._structureHash;
|
|
10147
10315
|
const latestArchive = await readLatestArchive(node.metaPath);
|
|
10148
10316
|
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10149
|
-
//
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
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;
|
|
10153
10327
|
// _crossRefs declaration change
|
|
10154
10328
|
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
10155
10329
|
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
@@ -10171,38 +10345,30 @@ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas
|
|
|
10171
10345
|
}
|
|
10172
10346
|
if (steerChanged)
|
|
10173
10347
|
architectInvalidators.push('steer');
|
|
10174
|
-
if (architectChanged)
|
|
10175
|
-
architectInvalidators.push('_architect');
|
|
10176
10348
|
if (crossRefsDeclChanged)
|
|
10177
10349
|
architectInvalidators.push('_crossRefs');
|
|
10178
|
-
if (
|
|
10350
|
+
if (effectiveSynthesisCount >= config.architectEvery) {
|
|
10179
10351
|
architectInvalidators.push('architectEvery');
|
|
10180
10352
|
}
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
if (architectInvalidators.length > 0
|
|
10353
|
+
if (!meta._builder)
|
|
10354
|
+
architectInvalidators.push('firstRun');
|
|
10355
|
+
if (architectInvalidators.length > 0) {
|
|
10184
10356
|
phaseState = invalidateArchitect(phaseState);
|
|
10185
10357
|
}
|
|
10186
10358
|
// ── Builder-level inputs ──
|
|
10187
|
-
// Scope file mtime check — if any file newer than _generatedAt
|
|
10188
|
-
const scopeMtimeMax = null;
|
|
10189
|
-
// Note: actual mtime check is done by the caller or via isStale;
|
|
10190
|
-
// here we just detect cross-ref content changes for the cascade.
|
|
10191
10359
|
// Cross-ref _content change (builder-invalidating)
|
|
10192
10360
|
let crossRefContentChanged = false;
|
|
10193
10361
|
return {
|
|
10194
10362
|
phaseState,
|
|
10195
10363
|
architectInvalidators,
|
|
10196
|
-
|
|
10364
|
+
inputStatus: {
|
|
10197
10365
|
structureHash,
|
|
10198
10366
|
steerChanged,
|
|
10199
10367
|
architectChanged,
|
|
10368
|
+
criticChanged,
|
|
10200
10369
|
crossRefsDeclChanged,
|
|
10201
|
-
scopeMtimeMax,
|
|
10202
10370
|
crossRefContentChanged,
|
|
10203
10371
|
},
|
|
10204
|
-
structureHash,
|
|
10205
|
-
steerChanged,
|
|
10206
10372
|
};
|
|
10207
10373
|
}
|
|
10208
10374
|
|
|
@@ -10382,6 +10548,15 @@ function toMetaError(step, err, code = 'FAILED') {
|
|
|
10382
10548
|
*
|
|
10383
10549
|
* @module orchestrator/parseOutput
|
|
10384
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
|
+
}
|
|
10385
10560
|
/**
|
|
10386
10561
|
* Parse architect output. The architect returns a task brief as text.
|
|
10387
10562
|
*
|
|
@@ -10389,7 +10564,7 @@ function toMetaError(step, err, code = 'FAILED') {
|
|
|
10389
10564
|
* @returns The task brief string.
|
|
10390
10565
|
*/
|
|
10391
10566
|
function parseArchitectOutput(output) {
|
|
10392
|
-
return output
|
|
10567
|
+
return stripSentinel(output);
|
|
10393
10568
|
}
|
|
10394
10569
|
/**
|
|
10395
10570
|
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
@@ -10400,7 +10575,7 @@ function parseArchitectOutput(output) {
|
|
|
10400
10575
|
* @returns Parsed builder output with content and structured fields.
|
|
10401
10576
|
*/
|
|
10402
10577
|
function parseBuilderOutput(output) {
|
|
10403
|
-
const trimmed = output
|
|
10578
|
+
const trimmed = stripSentinel(output);
|
|
10404
10579
|
// Strategy 1: Try to parse the entire output as JSON directly
|
|
10405
10580
|
const direct = tryParseJson(trimmed);
|
|
10406
10581
|
if (direct)
|
|
@@ -10467,7 +10642,7 @@ function tryParseJson(str) {
|
|
|
10467
10642
|
* @returns The feedback string.
|
|
10468
10643
|
*/
|
|
10469
10644
|
function parseCriticOutput(output) {
|
|
10470
|
-
return output
|
|
10645
|
+
return stripSentinel(output);
|
|
10471
10646
|
}
|
|
10472
10647
|
|
|
10473
10648
|
/**
|
|
@@ -10478,6 +10653,12 @@ function parseCriticOutput(output) {
|
|
|
10478
10653
|
*
|
|
10479
10654
|
* @module orchestrator/runPhase
|
|
10480
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
|
+
}
|
|
10481
10662
|
/** Write updated meta with phase state via lock staging. */
|
|
10482
10663
|
async function persistPhaseState(base, phaseState, updates) {
|
|
10483
10664
|
const lockPath = join(base.metaPath, '.lock');
|
|
@@ -10521,6 +10702,12 @@ async function handlePhaseFailure(phase, err, executor, ps, base, additionalUpda
|
|
|
10521
10702
|
// ── Architect executor ─────────────────────────────────────────────────
|
|
10522
10703
|
async function runArchitect(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10523
10704
|
let ps = phaseRunning(phaseState, 'architect');
|
|
10705
|
+
const base = {
|
|
10706
|
+
metaPath: node.metaPath,
|
|
10707
|
+
current: currentMeta,
|
|
10708
|
+
config,
|
|
10709
|
+
structureHash,
|
|
10710
|
+
};
|
|
10524
10711
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10525
10712
|
try {
|
|
10526
10713
|
await onProgress?.({
|
|
@@ -10532,21 +10719,25 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
10532
10719
|
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
10533
10720
|
const result = await executor.spawn(architectTask, {
|
|
10534
10721
|
thinking: config.thinking,
|
|
10535
|
-
timeout: config.architectTimeout,
|
|
10722
|
+
timeout: currentMeta._architectTimeout ?? config.architectTimeout,
|
|
10536
10723
|
label: 'meta-architect',
|
|
10537
10724
|
});
|
|
10538
10725
|
const builderBrief = parseArchitectOutput(result.output);
|
|
10539
10726
|
const architectTokens = result.tokens;
|
|
10540
10727
|
// Architect success: architect → fresh, _synthesisCount → 0
|
|
10541
10728
|
ps = architectSuccess(ps);
|
|
10542
|
-
const
|
|
10729
|
+
const architectUpdates = {
|
|
10543
10730
|
_builder: builderBrief,
|
|
10544
|
-
_architect:
|
|
10731
|
+
_architect: config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
|
|
10545
10732
|
_synthesisCount: 0,
|
|
10546
10733
|
_architectTokens: architectTokens,
|
|
10547
10734
|
_generatedAt: new Date().toISOString(),
|
|
10548
10735
|
_error: undefined,
|
|
10549
|
-
}
|
|
10736
|
+
};
|
|
10737
|
+
const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
|
|
10738
|
+
if (ancestorHash)
|
|
10739
|
+
architectUpdates._ancestorBuilderHash = ancestorHash;
|
|
10740
|
+
const updatedMeta = await persistPhaseState(base, ps, architectUpdates);
|
|
10550
10741
|
await onProgress?.({
|
|
10551
10742
|
type: 'phase_complete',
|
|
10552
10743
|
path: node.ownerPath,
|
|
@@ -10557,16 +10748,18 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
10557
10748
|
return { executed: true, phaseState: ps, updatedMeta };
|
|
10558
10749
|
}
|
|
10559
10750
|
catch (err) {
|
|
10560
|
-
return handlePhaseFailure('architect', err, executor, ps,
|
|
10561
|
-
metaPath: node.metaPath,
|
|
10562
|
-
current: currentMeta,
|
|
10563
|
-
structureHash,
|
|
10564
|
-
});
|
|
10751
|
+
return handlePhaseFailure('architect', err, executor, ps, base);
|
|
10565
10752
|
}
|
|
10566
10753
|
}
|
|
10567
10754
|
// ── Builder executor ───────────────────────────────────────────────────
|
|
10568
10755
|
async function runBuilder(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10569
10756
|
let ps = phaseRunning(phaseState, 'builder');
|
|
10757
|
+
const base = {
|
|
10758
|
+
metaPath: node.metaPath,
|
|
10759
|
+
current: currentMeta,
|
|
10760
|
+
config,
|
|
10761
|
+
structureHash,
|
|
10762
|
+
};
|
|
10570
10763
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10571
10764
|
try {
|
|
10572
10765
|
await onProgress?.({
|
|
@@ -10578,21 +10771,25 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
10578
10771
|
const builderTask = buildBuilderTask(ctx, currentMeta, config);
|
|
10579
10772
|
const result = await executor.spawn(builderTask, {
|
|
10580
10773
|
thinking: config.thinking,
|
|
10581
|
-
timeout: config.builderTimeout,
|
|
10774
|
+
timeout: currentMeta._builderTimeout ?? config.builderTimeout,
|
|
10582
10775
|
label: 'meta-builder',
|
|
10583
10776
|
});
|
|
10584
10777
|
const builderOutput = parseBuilderOutput(result.output);
|
|
10585
10778
|
const builderTokens = result.tokens;
|
|
10586
10779
|
// Builder success: builder → fresh, critic → pending
|
|
10587
10780
|
ps = builderSuccess(ps);
|
|
10588
|
-
const
|
|
10781
|
+
const builderUpdates = {
|
|
10589
10782
|
_content: builderOutput.content,
|
|
10590
10783
|
_state: builderOutput.state,
|
|
10591
10784
|
_builderTokens: builderTokens,
|
|
10592
10785
|
_generatedAt: new Date().toISOString(),
|
|
10593
10786
|
_error: undefined,
|
|
10594
10787
|
...builderOutput.fields,
|
|
10595
|
-
}
|
|
10788
|
+
};
|
|
10789
|
+
const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
|
|
10790
|
+
if (ancestorHash)
|
|
10791
|
+
builderUpdates._ancestorBuilderHash = ancestorHash;
|
|
10792
|
+
const updatedMeta = await persistPhaseState(base, ps, builderUpdates);
|
|
10596
10793
|
await onProgress?.({
|
|
10597
10794
|
type: 'phase_complete',
|
|
10598
10795
|
path: node.ownerPath,
|
|
@@ -10618,16 +10815,18 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
10618
10815
|
// Could not read partial output — no state recovery
|
|
10619
10816
|
}
|
|
10620
10817
|
}
|
|
10621
|
-
return handlePhaseFailure('builder', err, executor, ps,
|
|
10622
|
-
metaPath: node.metaPath,
|
|
10623
|
-
current: currentMeta,
|
|
10624
|
-
structureHash,
|
|
10625
|
-
}, partialState);
|
|
10818
|
+
return handlePhaseFailure('builder', err, executor, ps, base, partialState);
|
|
10626
10819
|
}
|
|
10627
10820
|
}
|
|
10628
10821
|
// ── Critic executor ────────────────────────────────────────────────────
|
|
10629
10822
|
async function runCritic(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10630
10823
|
let ps = phaseRunning(phaseState, 'critic');
|
|
10824
|
+
const base = {
|
|
10825
|
+
metaPath: node.metaPath,
|
|
10826
|
+
current: currentMeta,
|
|
10827
|
+
config,
|
|
10828
|
+
structureHash,
|
|
10829
|
+
};
|
|
10631
10830
|
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10632
10831
|
// Build critic task using current meta's _content
|
|
10633
10832
|
const metaForCritic = { ...currentMeta };
|
|
@@ -10641,7 +10840,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
10641
10840
|
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
10642
10841
|
const result = await executor.spawn(criticTask, {
|
|
10643
10842
|
thinking: config.thinking,
|
|
10644
|
-
timeout: config.criticTimeout,
|
|
10843
|
+
timeout: currentMeta._criticTimeout ?? config.criticTimeout,
|
|
10645
10844
|
label: 'meta-critic',
|
|
10646
10845
|
});
|
|
10647
10846
|
const feedback = parseCriticOutput(result.output);
|
|
@@ -10651,6 +10850,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
10651
10850
|
const cycleComplete = isFullyFresh(ps);
|
|
10652
10851
|
const updates = {
|
|
10653
10852
|
_feedback: feedback,
|
|
10853
|
+
_critic: config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
10654
10854
|
_criticTokens: criticTokens,
|
|
10655
10855
|
_error: undefined,
|
|
10656
10856
|
};
|
|
@@ -10659,7 +10859,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
10659
10859
|
if (cycleComplete) {
|
|
10660
10860
|
updates._synthesisCount = (currentMeta._synthesisCount ?? 0) + 1;
|
|
10661
10861
|
}
|
|
10662
|
-
const updatedMeta = await persistPhaseState(
|
|
10862
|
+
const updatedMeta = await persistPhaseState(base, ps, updates);
|
|
10663
10863
|
// Archive on full-cycle only
|
|
10664
10864
|
if (cycleComplete) {
|
|
10665
10865
|
await createSnapshot(node.metaPath, updatedMeta);
|
|
@@ -10680,11 +10880,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
10680
10880
|
};
|
|
10681
10881
|
}
|
|
10682
10882
|
catch (err) {
|
|
10683
|
-
return handlePhaseFailure('critic', err, executor, ps,
|
|
10684
|
-
metaPath: node.metaPath,
|
|
10685
|
-
current: currentMeta,
|
|
10686
|
-
structureHash,
|
|
10687
|
-
});
|
|
10883
|
+
return handlePhaseFailure('critic', err, executor, ps, base);
|
|
10688
10884
|
}
|
|
10689
10885
|
}
|
|
10690
10886
|
|
|
@@ -11261,12 +11457,6 @@ class WatcherHealthCheck {
|
|
|
11261
11457
|
return;
|
|
11262
11458
|
}
|
|
11263
11459
|
const data = (await res.json());
|
|
11264
|
-
// If rules were never successfully registered (startup failure),
|
|
11265
|
-
// attempt registration now that the watcher is reachable.
|
|
11266
|
-
if (!this.registrar.isRegistered) {
|
|
11267
|
-
this.logger.info('Rules not registered — attempting registration');
|
|
11268
|
-
await this.registrar.register();
|
|
11269
|
-
}
|
|
11270
11460
|
await this.registrar.checkAndReregister(data.uptime);
|
|
11271
11461
|
}
|
|
11272
11462
|
catch (err) {
|
|
@@ -11419,36 +11609,39 @@ class RuleRegistrar {
|
|
|
11419
11609
|
logger;
|
|
11420
11610
|
watcherClient;
|
|
11421
11611
|
lastWatcherUptime = null;
|
|
11422
|
-
|
|
11612
|
+
registering = false;
|
|
11423
11613
|
constructor(config, logger, watcher) {
|
|
11424
11614
|
this.config = config;
|
|
11425
11615
|
this.logger = logger;
|
|
11426
11616
|
this.watcherClient = watcher;
|
|
11427
11617
|
}
|
|
11428
|
-
/** Whether rules have been successfully registered. */
|
|
11429
|
-
get isRegistered() {
|
|
11430
|
-
return this.registered;
|
|
11431
|
-
}
|
|
11432
11618
|
/**
|
|
11433
11619
|
* Register rules with watcher. Retries with exponential backoff.
|
|
11434
11620
|
* Non-blocking — logs errors but never throws.
|
|
11435
11621
|
*/
|
|
11436
11622
|
async register() {
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
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
|
+
}
|
|
11449
11639
|
}
|
|
11640
|
+
this.logger.error('Rule registration failed after max retries — service degraded');
|
|
11641
|
+
}
|
|
11642
|
+
finally {
|
|
11643
|
+
this.registering = false;
|
|
11450
11644
|
}
|
|
11451
|
-
this.logger.error('Rule registration failed after max retries — service degraded');
|
|
11452
11645
|
}
|
|
11453
11646
|
/**
|
|
11454
11647
|
* Check watcher uptime and re-register if it decreased (restart detected).
|
|
@@ -11459,7 +11652,6 @@ class RuleRegistrar {
|
|
|
11459
11652
|
if (this.lastWatcherUptime !== null &&
|
|
11460
11653
|
currentUptime < this.lastWatcherUptime) {
|
|
11461
11654
|
this.logger.info({ previous: this.lastWatcherUptime, current: currentUptime }, 'Watcher restart detected — re-registering rules');
|
|
11462
|
-
this.registered = false;
|
|
11463
11655
|
await this.register();
|
|
11464
11656
|
}
|
|
11465
11657
|
this.lastWatcherUptime = currentUptime;
|
|
@@ -11529,6 +11721,12 @@ async function createMeta(ownerPath, options) {
|
|
|
11529
11721
|
metaJson._crossRefs = options.crossRefs;
|
|
11530
11722
|
if (options?.steer !== undefined)
|
|
11531
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;
|
|
11532
11730
|
const metaJsonPath = join(metaDir, 'meta.json');
|
|
11533
11731
|
await writeFile(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
11534
11732
|
return { metaDir, _id };
|
|
@@ -11557,15 +11755,24 @@ function metaExists(ownerPath) {
|
|
|
11557
11755
|
/**
|
|
11558
11756
|
* Extract parent directory paths from watcher walk results.
|
|
11559
11757
|
*
|
|
11560
|
-
* Walk returns file paths; we need the unique set of
|
|
11561
|
-
*
|
|
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.
|
|
11562
11762
|
*/
|
|
11563
|
-
function extractDirectories(filePaths, logger) {
|
|
11763
|
+
function extractDirectories(filePaths, parentDepth = 0, logger) {
|
|
11564
11764
|
const dirs = new Set();
|
|
11565
11765
|
for (const fp of filePaths) {
|
|
11566
11766
|
// Normalize backslash paths (Windows) to forward slashes before posix.dirname
|
|
11567
11767
|
const normalized = normalizePath(fp);
|
|
11568
|
-
|
|
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
|
+
}
|
|
11569
11776
|
if (dir !== '.' && dir !== '/') {
|
|
11570
11777
|
dirs.add(dir);
|
|
11571
11778
|
}
|
|
@@ -11590,11 +11797,14 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
11590
11797
|
const candidates = new Map();
|
|
11591
11798
|
for (const rule of rules) {
|
|
11592
11799
|
const files = await watcher.walk([rule.match]);
|
|
11593
|
-
const dirs = extractDirectories(files, logger);
|
|
11800
|
+
const dirs = extractDirectories(files, rule.parentDepth, logger);
|
|
11594
11801
|
for (const dir of dirs) {
|
|
11595
11802
|
candidates.set(dir, {
|
|
11596
11803
|
steer: rule.steer,
|
|
11597
11804
|
crossRefs: rule.crossRefs,
|
|
11805
|
+
architectTimeout: rule.architectTimeout,
|
|
11806
|
+
builderTimeout: rule.builderTimeout,
|
|
11807
|
+
criticTimeout: rule.criticTimeout,
|
|
11598
11808
|
});
|
|
11599
11809
|
}
|
|
11600
11810
|
}
|
|
@@ -11609,10 +11819,7 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
11609
11819
|
const seededPaths = [];
|
|
11610
11820
|
for (const candidate of toSeed) {
|
|
11611
11821
|
try {
|
|
11612
|
-
await createMeta(candidate.path,
|
|
11613
|
-
steer: candidate.steer,
|
|
11614
|
-
crossRefs: candidate.crossRefs,
|
|
11615
|
-
});
|
|
11822
|
+
await createMeta(candidate.path, candidate);
|
|
11616
11823
|
seededPaths.push(candidate.path);
|
|
11617
11824
|
logger?.info({ path: candidate.path }, 'auto-seeded meta');
|
|
11618
11825
|
}
|
|
@@ -11814,7 +12021,7 @@ class Scheduler {
|
|
|
11814
12021
|
metaPath: t2.node.metaPath,
|
|
11815
12022
|
current: currentMeta,
|
|
11816
12023
|
config: this.config,
|
|
11817
|
-
structureHash: result.structureHash,
|
|
12024
|
+
structureHash: result.inputStatus.structureHash,
|
|
11818
12025
|
}, result.phaseState, {});
|
|
11819
12026
|
this.cache.invalidate();
|
|
11820
12027
|
return {
|
|
@@ -11828,8 +12035,10 @@ class Scheduler {
|
|
|
11828
12035
|
metaPath: t2.node.metaPath,
|
|
11829
12036
|
current: currentMeta,
|
|
11830
12037
|
config: this.config,
|
|
11831
|
-
structureHash: result.structureHash,
|
|
11832
|
-
}, result.phaseState, {
|
|
12038
|
+
structureHash: result.inputStatus.structureHash,
|
|
12039
|
+
}, result.phaseState, {
|
|
12040
|
+
_generatedAt: new Date().toISOString(),
|
|
12041
|
+
});
|
|
11833
12042
|
dirty = true;
|
|
11834
12043
|
}
|
|
11835
12044
|
finally {
|
|
@@ -11859,7 +12068,7 @@ function sanitizeConfig(config) {
|
|
|
11859
12068
|
}
|
|
11860
12069
|
function registerConfigRoute(app, deps) {
|
|
11861
12070
|
const configHandler = createConfigQueryHandler(() => sanitizeConfig(deps.config));
|
|
11862
|
-
app.get('
|
|
12071
|
+
app.get(getEndpoint('config').path, async (request, reply) => {
|
|
11863
12072
|
const { path } = request.query;
|
|
11864
12073
|
const result = await configHandler({ path });
|
|
11865
12074
|
return reply.status(result.status).send(result.body);
|
|
@@ -11878,7 +12087,7 @@ function registerConfigRoute(app, deps) {
|
|
|
11878
12087
|
*/
|
|
11879
12088
|
/** Register the POST /config/apply route. */
|
|
11880
12089
|
function registerConfigApplyRoute(app, configPath) {
|
|
11881
|
-
app.post('
|
|
12090
|
+
app.post(getEndpoint('configApply').path, async (request, reply) => {
|
|
11882
12091
|
if (!configPath) {
|
|
11883
12092
|
return reply
|
|
11884
12093
|
.status(500)
|
|
@@ -11967,42 +12176,32 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
11967
12176
|
*
|
|
11968
12177
|
* @module routes/metas
|
|
11969
12178
|
*/
|
|
12179
|
+
/** Reusable Zod schema for boolean query string parameters ('true'/'false'). */
|
|
12180
|
+
const boolQueryParam = z.enum(['true', 'false']).transform((v) => v === 'true');
|
|
11970
12181
|
const metasQuerySchema = z.object({
|
|
11971
12182
|
pathPrefix: z.string().optional(),
|
|
11972
|
-
hasError:
|
|
11973
|
-
.enum(['true', 'false'])
|
|
11974
|
-
.transform((v) => v === 'true')
|
|
11975
|
-
.optional(),
|
|
12183
|
+
hasError: boolQueryParam.optional(),
|
|
11976
12184
|
staleHours: z
|
|
11977
12185
|
.string()
|
|
11978
12186
|
.transform(Number)
|
|
11979
12187
|
.pipe(z.number().positive())
|
|
11980
12188
|
.optional(),
|
|
11981
|
-
neverSynthesized:
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
.optional(),
|
|
11985
|
-
locked: z
|
|
11986
|
-
.enum(['true', 'false'])
|
|
11987
|
-
.transform((v) => v === 'true')
|
|
11988
|
-
.optional(),
|
|
11989
|
-
disabled: z
|
|
11990
|
-
.enum(['true', 'false'])
|
|
11991
|
-
.transform((v) => v === 'true')
|
|
11992
|
-
.optional(),
|
|
12189
|
+
neverSynthesized: boolQueryParam.optional(),
|
|
12190
|
+
locked: boolQueryParam.optional(),
|
|
12191
|
+
disabled: boolQueryParam.optional(),
|
|
11993
12192
|
fields: z.string().optional(),
|
|
11994
12193
|
});
|
|
11995
12194
|
const metaDetailQuerySchema = z.object({
|
|
11996
12195
|
fields: z.string().optional(),
|
|
11997
12196
|
includeArchive: z
|
|
11998
12197
|
.union([
|
|
11999
|
-
|
|
12198
|
+
boolQueryParam,
|
|
12000
12199
|
z.string().transform(Number).pipe(z.number().int().nonnegative()),
|
|
12001
12200
|
])
|
|
12002
12201
|
.optional(),
|
|
12003
12202
|
});
|
|
12004
12203
|
function registerMetasRoutes(app, deps) {
|
|
12005
|
-
app.get('
|
|
12204
|
+
app.get(getEndpoint('listMetas').path, async (request) => {
|
|
12006
12205
|
const query = metasQuerySchema.parse(request.query);
|
|
12007
12206
|
const { config, watcher } = deps;
|
|
12008
12207
|
const result = await listMetas(config, watcher);
|
|
@@ -12074,7 +12273,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
12074
12273
|
});
|
|
12075
12274
|
return { summary, metas };
|
|
12076
12275
|
});
|
|
12077
|
-
app.get('
|
|
12276
|
+
app.get(getEndpoint('metaDetail').path, async (request, reply) => {
|
|
12078
12277
|
const query = metaDetailQuerySchema.parse(request.query);
|
|
12079
12278
|
const { config, watcher } = deps;
|
|
12080
12279
|
const targetPath = normalizePath(decodeURIComponent(request.params.path));
|
|
@@ -12168,7 +12367,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
12168
12367
|
/**
|
|
12169
12368
|
* PATCH /metas/:path — update user-settable reserved properties on a meta.
|
|
12170
12369
|
*
|
|
12171
|
-
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled.
|
|
12370
|
+
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled, _architectTimeout, _builderTimeout, _criticTimeout.
|
|
12172
12371
|
* Set a field to null to remove it. Unknown keys are rejected.
|
|
12173
12372
|
*
|
|
12174
12373
|
* @module routes/metasUpdate
|
|
@@ -12180,10 +12379,13 @@ const updateBodySchema = z
|
|
|
12180
12379
|
_depth: z.union([z.number(), z.null()]).optional(),
|
|
12181
12380
|
_crossRefs: z.union([z.array(z.string()), z.null()]).optional(),
|
|
12182
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(),
|
|
12183
12385
|
})
|
|
12184
12386
|
.strict();
|
|
12185
12387
|
function registerMetasUpdateRoute(app, deps) {
|
|
12186
|
-
app.patch('
|
|
12388
|
+
app.patch(getEndpoint('updateMeta').path, async (request, reply) => {
|
|
12187
12389
|
const parseResult = updateBodySchema.safeParse(request.body);
|
|
12188
12390
|
if (!parseResult.success) {
|
|
12189
12391
|
return reply.status(400).send({
|
|
@@ -12205,13 +12407,7 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12205
12407
|
});
|
|
12206
12408
|
}
|
|
12207
12409
|
const metaJsonPath = join(metaDir, 'meta.json');
|
|
12208
|
-
const KEYS =
|
|
12209
|
-
'_steer',
|
|
12210
|
-
'_emphasis',
|
|
12211
|
-
'_depth',
|
|
12212
|
-
'_crossRefs',
|
|
12213
|
-
'_disabled',
|
|
12214
|
-
];
|
|
12410
|
+
const KEYS = Object.keys(updateBodySchema.shape);
|
|
12215
12411
|
const toDelete = new Set();
|
|
12216
12412
|
const toSet = {};
|
|
12217
12413
|
for (const key of KEYS) {
|
|
@@ -12249,7 +12445,7 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12249
12445
|
* @module routes/preview
|
|
12250
12446
|
*/
|
|
12251
12447
|
function registerPreviewRoute(app, deps) {
|
|
12252
|
-
app.get('
|
|
12448
|
+
app.get(getEndpoint('preview').path, async (request, reply) => {
|
|
12253
12449
|
const { config, watcher, cache } = deps;
|
|
12254
12450
|
const query = request.query;
|
|
12255
12451
|
let result;
|
|
@@ -12287,12 +12483,8 @@ function registerPreviewRoute(app, deps) {
|
|
|
12287
12483
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
12288
12484
|
// Compute invalidation inputs (DRY: reuse phaseState/invalidate logic)
|
|
12289
12485
|
const invalidation = await computeInvalidation(meta, scopeFiles, config, targetNode);
|
|
12290
|
-
const { architectInvalidators,
|
|
12291
|
-
const
|
|
12292
|
-
const structureChanged = structureHash !== meta._structureHash;
|
|
12293
|
-
const { steerChanged } = invalidation;
|
|
12294
|
-
const { architectChanged, crossRefsDeclChanged } = stalenessInputs;
|
|
12295
|
-
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
12486
|
+
const { architectInvalidators, inputStatus, phaseState } = invalidation;
|
|
12487
|
+
const architectTriggered = architectInvalidators.length > 0;
|
|
12296
12488
|
// Delta files
|
|
12297
12489
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
12298
12490
|
// EMA token estimates
|
|
@@ -12306,14 +12498,6 @@ function registerPreviewRoute(app, deps) {
|
|
|
12306
12498
|
? Math.round((Date.now() - new Date(meta._generatedAt).getTime()) / 1000)
|
|
12307
12499
|
: null;
|
|
12308
12500
|
const stalenessScore = computeStalenessScore(stalenessSeconds, meta._depth ?? 0, meta._emphasis ?? 1, config.depthWeight);
|
|
12309
|
-
// Phase state
|
|
12310
|
-
const phaseState = derivePhaseState(meta, {
|
|
12311
|
-
structureChanged,
|
|
12312
|
-
steerChanged,
|
|
12313
|
-
architectChanged,
|
|
12314
|
-
crossRefsChanged: crossRefsDeclChanged,
|
|
12315
|
-
architectEvery: config.architectEvery,
|
|
12316
|
-
});
|
|
12317
12501
|
const owedPhase = getOwedPhase(phaseState);
|
|
12318
12502
|
const priorityBand = getPriorityBand(phaseState);
|
|
12319
12503
|
return {
|
|
@@ -12324,10 +12508,17 @@ function registerPreviewRoute(app, deps) {
|
|
|
12324
12508
|
},
|
|
12325
12509
|
architectWillRun: architectTriggered,
|
|
12326
12510
|
architectReason: [
|
|
12327
|
-
...(
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
...((
|
|
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')
|
|
12331
12522
|
? ['periodic refresh']
|
|
12332
12523
|
: []),
|
|
12333
12524
|
].join(', ') || 'not triggered',
|
|
@@ -12344,7 +12535,7 @@ function registerPreviewRoute(app, deps) {
|
|
|
12344
12535
|
owedPhase,
|
|
12345
12536
|
priorityBand,
|
|
12346
12537
|
phaseState,
|
|
12347
|
-
|
|
12538
|
+
inputStatus,
|
|
12348
12539
|
architectInvalidators,
|
|
12349
12540
|
};
|
|
12350
12541
|
});
|
|
@@ -12362,7 +12553,7 @@ function registerPreviewRoute(app, deps) {
|
|
|
12362
12553
|
/** Register queue management routes. */
|
|
12363
12554
|
function registerQueueRoutes(app, deps) {
|
|
12364
12555
|
const { queue } = deps;
|
|
12365
|
-
app.get('
|
|
12556
|
+
app.get(getEndpoint('queue').path, async () => {
|
|
12366
12557
|
const currentPhase = queue.currentPhase;
|
|
12367
12558
|
const overrides = queue.overrides;
|
|
12368
12559
|
// Compute owedPhase for each override entry by reading meta state
|
|
@@ -12438,11 +12629,11 @@ function registerQueueRoutes(app, deps) {
|
|
|
12438
12629
|
state: queue.getState(),
|
|
12439
12630
|
};
|
|
12440
12631
|
});
|
|
12441
|
-
app.post('
|
|
12632
|
+
app.post(getEndpoint('queueClear').path, () => {
|
|
12442
12633
|
const removed = queue.clearOverrides();
|
|
12443
12634
|
return { cleared: removed };
|
|
12444
12635
|
});
|
|
12445
|
-
app.post('
|
|
12636
|
+
app.post(getEndpoint('abort').path, async (_request, reply) => {
|
|
12446
12637
|
// Check 3-layer current first
|
|
12447
12638
|
const currentPhase = queue.currentPhase;
|
|
12448
12639
|
const current = currentPhase ?? queue.current;
|
|
@@ -12504,7 +12695,7 @@ const seedBodySchema = z.object({
|
|
|
12504
12695
|
steer: z.string().optional(),
|
|
12505
12696
|
});
|
|
12506
12697
|
function registerSeedRoute(app, deps) {
|
|
12507
|
-
app.post('
|
|
12698
|
+
app.post(getEndpoint('seed').path, async (request, reply) => {
|
|
12508
12699
|
const body = seedBodySchema.parse(request.body);
|
|
12509
12700
|
if (metaExists(body.path)) {
|
|
12510
12701
|
return reply.status(409).send({
|
|
@@ -12641,7 +12832,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12641
12832
|
dependencies: {
|
|
12642
12833
|
watcher: {
|
|
12643
12834
|
...watcherHealth,
|
|
12644
|
-
rulesRegistered:
|
|
12835
|
+
rulesRegistered: true,
|
|
12645
12836
|
},
|
|
12646
12837
|
gateway: gatewayHealth,
|
|
12647
12838
|
},
|
|
@@ -12650,7 +12841,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12650
12841
|
};
|
|
12651
12842
|
},
|
|
12652
12843
|
});
|
|
12653
|
-
app.get('
|
|
12844
|
+
app.get(getEndpoint('status').path, async (_request, reply) => {
|
|
12654
12845
|
const result = await statusHandler();
|
|
12655
12846
|
return reply.status(result.status).send(result.body);
|
|
12656
12847
|
});
|
|
@@ -12669,24 +12860,41 @@ const synthesizeBodySchema = z.object({
|
|
|
12669
12860
|
});
|
|
12670
12861
|
/** Register the POST /synthesize route. */
|
|
12671
12862
|
function registerSynthesizeRoute(app, deps) {
|
|
12672
|
-
app.post('
|
|
12863
|
+
app.post(getEndpoint('synthesize').path, async (request, reply) => {
|
|
12673
12864
|
const body = synthesizeBodySchema.parse(request.body);
|
|
12674
12865
|
const { config, watcher, queue, cache } = deps;
|
|
12675
12866
|
if (body.path) {
|
|
12676
12867
|
// Path-targeted trigger: create override entry
|
|
12677
12868
|
const targetPath = resolveMetaDir(body.path);
|
|
12678
|
-
// 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.
|
|
12679
12872
|
let owedPhase = null;
|
|
12680
12873
|
let meta;
|
|
12681
12874
|
try {
|
|
12682
12875
|
meta = await readMetaJson(targetPath);
|
|
12683
|
-
const
|
|
12684
|
-
|
|
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
|
+
}
|
|
12685
12892
|
}
|
|
12686
12893
|
catch {
|
|
12687
|
-
// Meta unreadable — proceed,
|
|
12894
|
+
// Meta unreadable or watcher unavailable — proceed,
|
|
12895
|
+
// phase will be evaluated at dequeue time
|
|
12688
12896
|
}
|
|
12689
|
-
// Fully fresh meta → skip
|
|
12897
|
+
// Fully fresh meta → skip
|
|
12690
12898
|
if (owedPhase === null && meta && (meta._phaseState || meta._content)) {
|
|
12691
12899
|
return await reply.code(200).send({
|
|
12692
12900
|
status: 'skipped',
|
|
@@ -12744,7 +12952,7 @@ const unlockBodySchema = z.object({
|
|
|
12744
12952
|
path: z.string().min(1),
|
|
12745
12953
|
});
|
|
12746
12954
|
function registerUnlockRoute(app, deps) {
|
|
12747
|
-
app.post('
|
|
12955
|
+
app.post(getEndpoint('unlock').path, (request, reply) => {
|
|
12748
12956
|
const body = unlockBodySchema.parse(request.body);
|
|
12749
12957
|
const metaDir = resolveMetaDir(body.path);
|
|
12750
12958
|
const lockPath = join(metaDir, '.lock');
|
|
@@ -13141,9 +13349,7 @@ async function startService(config, configPath) {
|
|
|
13141
13349
|
routeDeps.registrar = registrar;
|
|
13142
13350
|
void registrar.register().then(() => {
|
|
13143
13351
|
routeDeps.ready = true;
|
|
13144
|
-
|
|
13145
|
-
void verifyRuleApplication(watcher, logger);
|
|
13146
|
-
}
|
|
13352
|
+
void verifyRuleApplication(watcher, logger);
|
|
13147
13353
|
}, () => {
|
|
13148
13354
|
// Registration failed after max retries — mark ready anyway
|
|
13149
13355
|
routeDeps.ready = true;
|
|
@@ -13336,6 +13542,17 @@ const metaJsonSchema = z
|
|
|
13336
13542
|
_error: metaErrorSchema.optional(),
|
|
13337
13543
|
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
13338
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(),
|
|
13339
13556
|
/**
|
|
13340
13557
|
* Per-phase state machine record. Engine-managed.
|
|
13341
13558
|
* Keyed by phase name (architect, builder, critic) with status values.
|