@karmaniverous/jeeves-meta 0.15.11 → 0.15.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.d.ts +11 -0
- package/dist/cli/jeeves-meta/index.js +111 -6
- package/dist/index.js +111 -6
- package/dist/orchestrator/orchestratePhase.d.ts +7 -1
- package/package.json +1 -1
package/dist/bootstrap.d.ts
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
* @module bootstrap
|
|
5
5
|
*/
|
|
6
6
|
import { type ServiceConfig } from './schema/config.js';
|
|
7
|
+
/**
|
|
8
|
+
* Compute per-cycle token total from a completed meta.
|
|
9
|
+
*
|
|
10
|
+
* Exported for testing.
|
|
11
|
+
*
|
|
12
|
+
* Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
|
|
13
|
+
* a value of 1 means architect ran this cycle (was 0 pre-increment),
|
|
14
|
+
* so all three phase token fields are summed. A value \> 1 means architect
|
|
15
|
+
* was skipped (cached brief reused), so only builder + critic are summed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function computeCycleTokens(meta: Record<string, unknown>): number;
|
|
7
18
|
/**
|
|
8
19
|
* Bootstrap the service: create logger, build server, start listening,
|
|
9
20
|
* wire scheduler, queue processing, rule registration, config hot-reload,
|
|
@@ -2337,7 +2337,7 @@ function buildPhaseCandidates(entries, architectEvery) {
|
|
|
2337
2337
|
if (entry.meta._phaseState) {
|
|
2338
2338
|
const needsArchitect = !entry.meta._builder ||
|
|
2339
2339
|
(entry.meta._synthesisCount ?? 0) >= architectEvery;
|
|
2340
|
-
if (needsArchitect && ps
|
|
2340
|
+
if (needsArchitect && isFullyFresh(ps)) {
|
|
2341
2341
|
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
2342
2342
|
}
|
|
2343
2343
|
}
|
|
@@ -2573,6 +2573,9 @@ async function persistPhaseState(base, phaseState, updates) {
|
|
|
2573
2573
|
_phaseState: phaseState,
|
|
2574
2574
|
_structureHash: base.structureHash,
|
|
2575
2575
|
};
|
|
2576
|
+
if (!merged._id) {
|
|
2577
|
+
merged._id = randomUUID();
|
|
2578
|
+
}
|
|
2576
2579
|
// Clean undefined
|
|
2577
2580
|
if (merged._error === undefined)
|
|
2578
2581
|
delete merged._error;
|
|
@@ -2678,7 +2681,33 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
2678
2681
|
timeout: currentMeta._builderTimeout ?? config.builderTimeout,
|
|
2679
2682
|
label: 'meta-builder',
|
|
2680
2683
|
});
|
|
2681
|
-
const
|
|
2684
|
+
const rawOutput = result.output;
|
|
2685
|
+
// Exact match only — ANNOUNCE_SKIP as the entire output means "no update."
|
|
2686
|
+
// A trailing sentinel on valid output (e.g. JSON + ANNOUNCE_SKIP) is handled
|
|
2687
|
+
// by stripSentinel() inside parseBuilderOutput and is NOT a skip.
|
|
2688
|
+
const isSkip = rawOutput.trim() === 'ANNOUNCE_SKIP';
|
|
2689
|
+
if (isSkip) {
|
|
2690
|
+
// ANNOUNCE_SKIP: preserve existing _content, bump _generatedAt only
|
|
2691
|
+
ps = builderSuccess(ps);
|
|
2692
|
+
const skipUpdates = {
|
|
2693
|
+
_builderTokens: result.tokens,
|
|
2694
|
+
_generatedAt: new Date().toISOString(),
|
|
2695
|
+
_error: undefined,
|
|
2696
|
+
};
|
|
2697
|
+
const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
|
|
2698
|
+
if (ancestorHash)
|
|
2699
|
+
skipUpdates._ancestorBuilderHash = ancestorHash;
|
|
2700
|
+
const updatedMeta = await persistPhaseState(base, ps, skipUpdates);
|
|
2701
|
+
await onProgress?.({
|
|
2702
|
+
type: 'phase_complete',
|
|
2703
|
+
path: node.ownerPath,
|
|
2704
|
+
phase: 'builder',
|
|
2705
|
+
tokens: result.tokens,
|
|
2706
|
+
durationMs: Date.now() - builderStart,
|
|
2707
|
+
});
|
|
2708
|
+
return { executed: true, phaseState: ps, updatedMeta };
|
|
2709
|
+
}
|
|
2710
|
+
const builderOutput = parseBuilderOutput(rawOutput);
|
|
2682
2711
|
const builderTokens = result.tokens;
|
|
2683
2712
|
// Builder success: builder → fresh, critic → pending
|
|
2684
2713
|
ps = builderSuccess(ps);
|
|
@@ -2798,6 +2827,28 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
2798
2827
|
*
|
|
2799
2828
|
* @module orchestrator/orchestratePhase
|
|
2800
2829
|
*/
|
|
2830
|
+
/**
|
|
2831
|
+
* Check whether a meta has an empty scope — no source files, no children,
|
|
2832
|
+
* no cross-refs, and no prior content. Matches §3.9 empty-scope criteria.
|
|
2833
|
+
*/
|
|
2834
|
+
function isEmptyScope(scopeFiles, node, meta) {
|
|
2835
|
+
return (scopeFiles.length === 0 &&
|
|
2836
|
+
node.children.length === 0 &&
|
|
2837
|
+
(!meta._crossRefs || meta._crossRefs.length === 0) &&
|
|
2838
|
+
!meta._content);
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Handle an empty-scope meta: set all phases fresh, bump _generatedAt.
|
|
2842
|
+
* Prevents perpetual staleness without wasting tokens on synthesis.
|
|
2843
|
+
*/
|
|
2844
|
+
async function handleEmptyScope(node, currentMeta, config, structureHash, logger) {
|
|
2845
|
+
await persistPhaseState({
|
|
2846
|
+
metaPath: node.metaPath,
|
|
2847
|
+
current: currentMeta,
|
|
2848
|
+
structureHash,
|
|
2849
|
+
}, freshPhaseState(), { _generatedAt: new Date().toISOString() });
|
|
2850
|
+
logger?.info({ path: node.ownerPath }, 'Empty scope — set all phases fresh, bumped _generatedAt');
|
|
2851
|
+
}
|
|
2801
2852
|
/** Phase runner dispatch map — avoids repeating the same switch/case. */
|
|
2802
2853
|
const phaseRunners = {
|
|
2803
2854
|
architect: runArchitect,
|
|
@@ -2853,6 +2904,11 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
|
|
|
2853
2904
|
// Compute structure hash for the phase
|
|
2854
2905
|
const { scopeFiles } = await getScopeFiles(winner.node, watcher);
|
|
2855
2906
|
const structureHash = computeStructureHash(scopeFiles);
|
|
2907
|
+
// Empty-scope guard (§3.9, #177): skip synthesis when nothing to synthesize
|
|
2908
|
+
if (isEmptyScope(scopeFiles, winner.node, currentMeta)) {
|
|
2909
|
+
await handleEmptyScope(winner.node, currentMeta, config, structureHash, logger);
|
|
2910
|
+
return { executed: false };
|
|
2911
|
+
}
|
|
2856
2912
|
// skipUnchanged: bump _generatedAt without altering _phaseState
|
|
2857
2913
|
if (config.skipUnchanged && currentMeta._generatedAt) {
|
|
2858
2914
|
const verifiedStale = await isStale(getScopePrefix(winner.node), currentMeta, watcher);
|
|
@@ -2894,6 +2950,11 @@ async function orchestrateTargeted(config, executor, watcher, targetPath, onProg
|
|
|
2894
2950
|
// Compute structure hash
|
|
2895
2951
|
const { scopeFiles } = await getScopeFiles(node, watcher);
|
|
2896
2952
|
const structureHash = computeStructureHash(scopeFiles);
|
|
2953
|
+
// Empty-scope guard (§3.9, #177): skip synthesis when nothing to synthesize
|
|
2954
|
+
if (isEmptyScope(scopeFiles, node, currentMeta)) {
|
|
2955
|
+
await handleEmptyScope(node, currentMeta, config, structureHash, logger);
|
|
2956
|
+
return { executed: false, metaPath: normalizedTarget };
|
|
2957
|
+
}
|
|
2897
2958
|
return await executePhase(node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
|
|
2898
2959
|
}
|
|
2899
2960
|
finally {
|
|
@@ -4056,6 +4117,12 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
4056
4117
|
.status(500)
|
|
4057
4118
|
.send({ error: `Failed to write config: ${message}` });
|
|
4058
4119
|
}
|
|
4120
|
+
// Compute whether any restart-required fields actually changed
|
|
4121
|
+
// (compares validated output against previous config, catching both
|
|
4122
|
+
// explicit patch keys and implicit changes via replace + schema defaults)
|
|
4123
|
+
const validatedRecord = validatedConfig;
|
|
4124
|
+
const restartRequired = RESTART_REQUIRED_FIELDS.some((field) => JSON.stringify(existing[field]) !==
|
|
4125
|
+
JSON.stringify(validatedRecord[field]));
|
|
4059
4126
|
// Apply hot-reload callback
|
|
4060
4127
|
try {
|
|
4061
4128
|
applyHotReloadedConfig(validatedConfig);
|
|
@@ -4065,11 +4132,12 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
4065
4132
|
return reply.status(200).send({
|
|
4066
4133
|
applied: true,
|
|
4067
4134
|
warning: `Config written but callback failed: ${message}`,
|
|
4068
|
-
restartRequired
|
|
4135
|
+
restartRequired,
|
|
4069
4136
|
});
|
|
4070
4137
|
}
|
|
4071
4138
|
return reply.status(200).send({
|
|
4072
4139
|
applied: true,
|
|
4140
|
+
restartRequired,
|
|
4073
4141
|
});
|
|
4074
4142
|
});
|
|
4075
4143
|
}
|
|
@@ -4330,6 +4398,7 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
4330
4398
|
}
|
|
4331
4399
|
Object.assign(updated, toSet);
|
|
4332
4400
|
await writeFile(metaJsonPath, JSON.stringify(updated, null, 2) + '\n');
|
|
4401
|
+
deps.cache.invalidate();
|
|
4333
4402
|
// Project the response — exclude the same large fields as the detail route.
|
|
4334
4403
|
const projected = {};
|
|
4335
4404
|
for (const [k, v] of Object.entries(updated)) {
|
|
@@ -4367,10 +4436,10 @@ function registerPreviewRoute(app, deps) {
|
|
|
4367
4436
|
const normalized = normalizePath(query.path);
|
|
4368
4437
|
targetNode = findNode(result.tree, normalized);
|
|
4369
4438
|
if (!targetNode) {
|
|
4370
|
-
return {
|
|
4439
|
+
return reply.status(404).send({
|
|
4371
4440
|
error: 'NOT_FOUND',
|
|
4372
4441
|
message: 'Meta path not found: ' + query.path,
|
|
4373
|
-
};
|
|
4442
|
+
});
|
|
4374
4443
|
}
|
|
4375
4444
|
}
|
|
4376
4445
|
else {
|
|
@@ -4940,7 +5009,7 @@ function registerRoutes(app, deps) {
|
|
|
4940
5009
|
});
|
|
4941
5010
|
registerStatusRoute(app, deps);
|
|
4942
5011
|
registerMetasRoutes(app, deps);
|
|
4943
|
-
registerMetasUpdateRoute(app);
|
|
5012
|
+
registerMetasUpdateRoute(app, deps);
|
|
4944
5013
|
registerSynthesizeRoute(app, deps);
|
|
4945
5014
|
registerPreviewRoute(app, deps);
|
|
4946
5015
|
registerSeedRoute(app, deps);
|
|
@@ -5027,6 +5096,18 @@ function registerShutdownHandlers(deps) {
|
|
|
5027
5096
|
deps.logger.warn({ path: current.path }, 'Failed to release lock during shutdown');
|
|
5028
5097
|
}
|
|
5029
5098
|
}
|
|
5099
|
+
// Release lock for in-progress override synthesis (only when it
|
|
5100
|
+
// differs from the legacy current item to avoid double-release)
|
|
5101
|
+
const currentPhase = deps.queue.currentPhase;
|
|
5102
|
+
if (currentPhase && currentPhase.path !== current?.path) {
|
|
5103
|
+
try {
|
|
5104
|
+
releaseLock(resolveMetaDir(currentPhase.path));
|
|
5105
|
+
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress override synthesis');
|
|
5106
|
+
}
|
|
5107
|
+
catch {
|
|
5108
|
+
deps.logger.warn({ path: currentPhase.path }, 'Failed to release override lock during shutdown');
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5030
5111
|
// 3. Close server
|
|
5031
5112
|
try {
|
|
5032
5113
|
await deps.server.close();
|
|
@@ -5124,6 +5205,25 @@ class HttpWatcherClient {
|
|
|
5124
5205
|
*
|
|
5125
5206
|
* @module bootstrap
|
|
5126
5207
|
*/
|
|
5208
|
+
/**
|
|
5209
|
+
* Compute per-cycle token total from a completed meta.
|
|
5210
|
+
*
|
|
5211
|
+
* Exported for testing.
|
|
5212
|
+
*
|
|
5213
|
+
* Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
|
|
5214
|
+
* a value of 1 means architect ran this cycle (was 0 pre-increment),
|
|
5215
|
+
* so all three phase token fields are summed. A value \> 1 means architect
|
|
5216
|
+
* was skipped (cached brief reused), so only builder + critic are summed.
|
|
5217
|
+
*/
|
|
5218
|
+
function computeCycleTokens(meta) {
|
|
5219
|
+
const builderTokens = meta._builderTokens ?? 0;
|
|
5220
|
+
const criticTokens = meta._criticTokens ?? 0;
|
|
5221
|
+
const architectRan = (meta._synthesisCount ?? 1) === 1;
|
|
5222
|
+
const architectTokens = architectRan
|
|
5223
|
+
? (meta._architectTokens ?? 0)
|
|
5224
|
+
: 0;
|
|
5225
|
+
return architectTokens + builderTokens + criticTokens;
|
|
5226
|
+
}
|
|
5127
5227
|
/**
|
|
5128
5228
|
* Bootstrap the service: create logger, build server, start listening,
|
|
5129
5229
|
* wire scheduler, queue processing, rule registration, config hot-reload,
|
|
@@ -5250,10 +5350,15 @@ async function startService(config, configPath) {
|
|
|
5250
5350
|
}
|
|
5251
5351
|
// Emit synthesis_complete only on full-cycle completion
|
|
5252
5352
|
if (result.cycleComplete) {
|
|
5353
|
+
const updatedMeta = result.phaseResult?.updatedMeta;
|
|
5354
|
+
const tokens = updatedMeta
|
|
5355
|
+
? computeCycleTokens(updatedMeta)
|
|
5356
|
+
: undefined;
|
|
5253
5357
|
await progress.report({
|
|
5254
5358
|
type: 'synthesis_complete',
|
|
5255
5359
|
path: ownerPath,
|
|
5256
5360
|
durationMs,
|
|
5361
|
+
tokens,
|
|
5257
5362
|
});
|
|
5258
5363
|
}
|
|
5259
5364
|
}
|
package/dist/index.js
CHANGED
|
@@ -2595,7 +2595,7 @@ function buildPhaseCandidates(entries, architectEvery) {
|
|
|
2595
2595
|
if (entry.meta._phaseState) {
|
|
2596
2596
|
const needsArchitect = !entry.meta._builder ||
|
|
2597
2597
|
(entry.meta._synthesisCount ?? 0) >= architectEvery;
|
|
2598
|
-
if (needsArchitect && ps
|
|
2598
|
+
if (needsArchitect && isFullyFresh(ps)) {
|
|
2599
2599
|
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
2600
2600
|
}
|
|
2601
2601
|
}
|
|
@@ -2831,6 +2831,9 @@ async function persistPhaseState(base, phaseState, updates) {
|
|
|
2831
2831
|
_phaseState: phaseState,
|
|
2832
2832
|
_structureHash: base.structureHash,
|
|
2833
2833
|
};
|
|
2834
|
+
if (!merged._id) {
|
|
2835
|
+
merged._id = randomUUID();
|
|
2836
|
+
}
|
|
2834
2837
|
// Clean undefined
|
|
2835
2838
|
if (merged._error === undefined)
|
|
2836
2839
|
delete merged._error;
|
|
@@ -2936,7 +2939,33 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
2936
2939
|
timeout: currentMeta._builderTimeout ?? config.builderTimeout,
|
|
2937
2940
|
label: 'meta-builder',
|
|
2938
2941
|
});
|
|
2939
|
-
const
|
|
2942
|
+
const rawOutput = result.output;
|
|
2943
|
+
// Exact match only — ANNOUNCE_SKIP as the entire output means "no update."
|
|
2944
|
+
// A trailing sentinel on valid output (e.g. JSON + ANNOUNCE_SKIP) is handled
|
|
2945
|
+
// by stripSentinel() inside parseBuilderOutput and is NOT a skip.
|
|
2946
|
+
const isSkip = rawOutput.trim() === 'ANNOUNCE_SKIP';
|
|
2947
|
+
if (isSkip) {
|
|
2948
|
+
// ANNOUNCE_SKIP: preserve existing _content, bump _generatedAt only
|
|
2949
|
+
ps = builderSuccess(ps);
|
|
2950
|
+
const skipUpdates = {
|
|
2951
|
+
_builderTokens: result.tokens,
|
|
2952
|
+
_generatedAt: new Date().toISOString(),
|
|
2953
|
+
_error: undefined,
|
|
2954
|
+
};
|
|
2955
|
+
const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
|
|
2956
|
+
if (ancestorHash)
|
|
2957
|
+
skipUpdates._ancestorBuilderHash = ancestorHash;
|
|
2958
|
+
const updatedMeta = await persistPhaseState(base, ps, skipUpdates);
|
|
2959
|
+
await onProgress?.({
|
|
2960
|
+
type: 'phase_complete',
|
|
2961
|
+
path: node.ownerPath,
|
|
2962
|
+
phase: 'builder',
|
|
2963
|
+
tokens: result.tokens,
|
|
2964
|
+
durationMs: Date.now() - builderStart,
|
|
2965
|
+
});
|
|
2966
|
+
return { executed: true, phaseState: ps, updatedMeta };
|
|
2967
|
+
}
|
|
2968
|
+
const builderOutput = parseBuilderOutput(rawOutput);
|
|
2940
2969
|
const builderTokens = result.tokens;
|
|
2941
2970
|
// Builder success: builder → fresh, critic → pending
|
|
2942
2971
|
ps = builderSuccess(ps);
|
|
@@ -3056,6 +3085,28 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
3056
3085
|
*
|
|
3057
3086
|
* @module orchestrator/orchestratePhase
|
|
3058
3087
|
*/
|
|
3088
|
+
/**
|
|
3089
|
+
* Check whether a meta has an empty scope — no source files, no children,
|
|
3090
|
+
* no cross-refs, and no prior content. Matches §3.9 empty-scope criteria.
|
|
3091
|
+
*/
|
|
3092
|
+
function isEmptyScope(scopeFiles, node, meta) {
|
|
3093
|
+
return (scopeFiles.length === 0 &&
|
|
3094
|
+
node.children.length === 0 &&
|
|
3095
|
+
(!meta._crossRefs || meta._crossRefs.length === 0) &&
|
|
3096
|
+
!meta._content);
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Handle an empty-scope meta: set all phases fresh, bump _generatedAt.
|
|
3100
|
+
* Prevents perpetual staleness without wasting tokens on synthesis.
|
|
3101
|
+
*/
|
|
3102
|
+
async function handleEmptyScope(node, currentMeta, config, structureHash, logger) {
|
|
3103
|
+
await persistPhaseState({
|
|
3104
|
+
metaPath: node.metaPath,
|
|
3105
|
+
current: currentMeta,
|
|
3106
|
+
structureHash,
|
|
3107
|
+
}, freshPhaseState(), { _generatedAt: new Date().toISOString() });
|
|
3108
|
+
logger?.info({ path: node.ownerPath }, 'Empty scope — set all phases fresh, bumped _generatedAt');
|
|
3109
|
+
}
|
|
3059
3110
|
/** Phase runner dispatch map — avoids repeating the same switch/case. */
|
|
3060
3111
|
const phaseRunners = {
|
|
3061
3112
|
architect: runArchitect,
|
|
@@ -3111,6 +3162,11 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
|
|
|
3111
3162
|
// Compute structure hash for the phase
|
|
3112
3163
|
const { scopeFiles } = await getScopeFiles(winner.node, watcher);
|
|
3113
3164
|
const structureHash = computeStructureHash(scopeFiles);
|
|
3165
|
+
// Empty-scope guard (§3.9, #177): skip synthesis when nothing to synthesize
|
|
3166
|
+
if (isEmptyScope(scopeFiles, winner.node, currentMeta)) {
|
|
3167
|
+
await handleEmptyScope(winner.node, currentMeta, config, structureHash, logger);
|
|
3168
|
+
return { executed: false };
|
|
3169
|
+
}
|
|
3114
3170
|
// skipUnchanged: bump _generatedAt without altering _phaseState
|
|
3115
3171
|
if (config.skipUnchanged && currentMeta._generatedAt) {
|
|
3116
3172
|
const verifiedStale = await isStale(getScopePrefix(winner.node), currentMeta, watcher);
|
|
@@ -3152,6 +3208,11 @@ async function orchestrateTargeted(config, executor, watcher, targetPath, onProg
|
|
|
3152
3208
|
// Compute structure hash
|
|
3153
3209
|
const { scopeFiles } = await getScopeFiles(node, watcher);
|
|
3154
3210
|
const structureHash = computeStructureHash(scopeFiles);
|
|
3211
|
+
// Empty-scope guard (§3.9, #177): skip synthesis when nothing to synthesize
|
|
3212
|
+
if (isEmptyScope(scopeFiles, node, currentMeta)) {
|
|
3213
|
+
await handleEmptyScope(node, currentMeta, config, structureHash, logger);
|
|
3214
|
+
return { executed: false, metaPath: normalizedTarget };
|
|
3215
|
+
}
|
|
3155
3216
|
return await executePhase(node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
|
|
3156
3217
|
}
|
|
3157
3218
|
finally {
|
|
@@ -4314,6 +4375,12 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
4314
4375
|
.status(500)
|
|
4315
4376
|
.send({ error: `Failed to write config: ${message}` });
|
|
4316
4377
|
}
|
|
4378
|
+
// Compute whether any restart-required fields actually changed
|
|
4379
|
+
// (compares validated output against previous config, catching both
|
|
4380
|
+
// explicit patch keys and implicit changes via replace + schema defaults)
|
|
4381
|
+
const validatedRecord = validatedConfig;
|
|
4382
|
+
const restartRequired = RESTART_REQUIRED_FIELDS.some((field) => JSON.stringify(existing[field]) !==
|
|
4383
|
+
JSON.stringify(validatedRecord[field]));
|
|
4317
4384
|
// Apply hot-reload callback
|
|
4318
4385
|
try {
|
|
4319
4386
|
applyHotReloadedConfig(validatedConfig);
|
|
@@ -4323,11 +4390,12 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
4323
4390
|
return reply.status(200).send({
|
|
4324
4391
|
applied: true,
|
|
4325
4392
|
warning: `Config written but callback failed: ${message}`,
|
|
4326
|
-
restartRequired
|
|
4393
|
+
restartRequired,
|
|
4327
4394
|
});
|
|
4328
4395
|
}
|
|
4329
4396
|
return reply.status(200).send({
|
|
4330
4397
|
applied: true,
|
|
4398
|
+
restartRequired,
|
|
4331
4399
|
});
|
|
4332
4400
|
});
|
|
4333
4401
|
}
|
|
@@ -4588,6 +4656,7 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
4588
4656
|
}
|
|
4589
4657
|
Object.assign(updated, toSet);
|
|
4590
4658
|
await writeFile(metaJsonPath, JSON.stringify(updated, null, 2) + '\n');
|
|
4659
|
+
deps.cache.invalidate();
|
|
4591
4660
|
// Project the response — exclude the same large fields as the detail route.
|
|
4592
4661
|
const projected = {};
|
|
4593
4662
|
for (const [k, v] of Object.entries(updated)) {
|
|
@@ -4625,10 +4694,10 @@ function registerPreviewRoute(app, deps) {
|
|
|
4625
4694
|
const normalized = normalizePath(query.path);
|
|
4626
4695
|
targetNode = findNode(result.tree, normalized);
|
|
4627
4696
|
if (!targetNode) {
|
|
4628
|
-
return {
|
|
4697
|
+
return reply.status(404).send({
|
|
4629
4698
|
error: 'NOT_FOUND',
|
|
4630
4699
|
message: 'Meta path not found: ' + query.path,
|
|
4631
|
-
};
|
|
4700
|
+
});
|
|
4632
4701
|
}
|
|
4633
4702
|
}
|
|
4634
4703
|
else {
|
|
@@ -5170,7 +5239,7 @@ function registerRoutes(app, deps) {
|
|
|
5170
5239
|
});
|
|
5171
5240
|
registerStatusRoute(app, deps);
|
|
5172
5241
|
registerMetasRoutes(app, deps);
|
|
5173
|
-
registerMetasUpdateRoute(app);
|
|
5242
|
+
registerMetasUpdateRoute(app, deps);
|
|
5174
5243
|
registerSynthesizeRoute(app, deps);
|
|
5175
5244
|
registerPreviewRoute(app, deps);
|
|
5176
5245
|
registerSeedRoute(app, deps);
|
|
@@ -5257,6 +5326,18 @@ function registerShutdownHandlers(deps) {
|
|
|
5257
5326
|
deps.logger.warn({ path: current.path }, 'Failed to release lock during shutdown');
|
|
5258
5327
|
}
|
|
5259
5328
|
}
|
|
5329
|
+
// Release lock for in-progress override synthesis (only when it
|
|
5330
|
+
// differs from the legacy current item to avoid double-release)
|
|
5331
|
+
const currentPhase = deps.queue.currentPhase;
|
|
5332
|
+
if (currentPhase && currentPhase.path !== current?.path) {
|
|
5333
|
+
try {
|
|
5334
|
+
releaseLock(resolveMetaDir(currentPhase.path));
|
|
5335
|
+
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress override synthesis');
|
|
5336
|
+
}
|
|
5337
|
+
catch {
|
|
5338
|
+
deps.logger.warn({ path: currentPhase.path }, 'Failed to release override lock during shutdown');
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5260
5341
|
// 3. Close server
|
|
5261
5342
|
try {
|
|
5262
5343
|
await deps.server.close();
|
|
@@ -5354,6 +5435,25 @@ class HttpWatcherClient {
|
|
|
5354
5435
|
*
|
|
5355
5436
|
* @module bootstrap
|
|
5356
5437
|
*/
|
|
5438
|
+
/**
|
|
5439
|
+
* Compute per-cycle token total from a completed meta.
|
|
5440
|
+
*
|
|
5441
|
+
* Exported for testing.
|
|
5442
|
+
*
|
|
5443
|
+
* Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
|
|
5444
|
+
* a value of 1 means architect ran this cycle (was 0 pre-increment),
|
|
5445
|
+
* so all three phase token fields are summed. A value \> 1 means architect
|
|
5446
|
+
* was skipped (cached brief reused), so only builder + critic are summed.
|
|
5447
|
+
*/
|
|
5448
|
+
function computeCycleTokens(meta) {
|
|
5449
|
+
const builderTokens = meta._builderTokens ?? 0;
|
|
5450
|
+
const criticTokens = meta._criticTokens ?? 0;
|
|
5451
|
+
const architectRan = (meta._synthesisCount ?? 1) === 1;
|
|
5452
|
+
const architectTokens = architectRan
|
|
5453
|
+
? (meta._architectTokens ?? 0)
|
|
5454
|
+
: 0;
|
|
5455
|
+
return architectTokens + builderTokens + criticTokens;
|
|
5456
|
+
}
|
|
5357
5457
|
/**
|
|
5358
5458
|
* Bootstrap the service: create logger, build server, start listening,
|
|
5359
5459
|
* wire scheduler, queue processing, rule registration, config hot-reload,
|
|
@@ -5480,10 +5580,15 @@ async function startService(config, configPath) {
|
|
|
5480
5580
|
}
|
|
5481
5581
|
// Emit synthesis_complete only on full-cycle completion
|
|
5482
5582
|
if (result.cycleComplete) {
|
|
5583
|
+
const updatedMeta = result.phaseResult?.updatedMeta;
|
|
5584
|
+
const tokens = updatedMeta
|
|
5585
|
+
? computeCycleTokens(updatedMeta)
|
|
5586
|
+
: undefined;
|
|
5483
5587
|
await progress.report({
|
|
5484
5588
|
type: 'synthesis_complete',
|
|
5485
5589
|
path: ownerPath,
|
|
5486
5590
|
durationMs,
|
|
5591
|
+
tokens,
|
|
5487
5592
|
});
|
|
5488
5593
|
}
|
|
5489
5594
|
}
|
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module orchestrator/orchestratePhase
|
|
10
10
|
*/
|
|
11
|
+
import type { MetaNode } from '../discovery/types.js';
|
|
11
12
|
import type { MetaExecutor, WatcherClient } from '../interfaces/index.js';
|
|
12
13
|
import type { MinimalLogger } from '../logger/index.js';
|
|
13
14
|
import type { ProgressEvent } from '../progress/index.js';
|
|
14
|
-
import type { MetaConfig, PhaseName } from '../schema/index.js';
|
|
15
|
+
import type { MetaConfig, MetaJson, PhaseName } from '../schema/index.js';
|
|
15
16
|
import { type PhaseResult } from './runPhase.js';
|
|
17
|
+
/**
|
|
18
|
+
* Check whether a meta has an empty scope — no source files, no children,
|
|
19
|
+
* no cross-refs, and no prior content. Matches §3.9 empty-scope criteria.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isEmptyScope(scopeFiles: string[], node: MetaNode, meta: MetaJson): boolean;
|
|
16
22
|
/** Callback for synthesis progress events. */
|
|
17
23
|
export type PhaseProgressCallback = (event: ProgressEvent) => void | Promise<void>;
|
|
18
24
|
/** Result of a single phase-aware orchestration tick. */
|
package/package.json
CHANGED