@karmaniverous/jeeves-meta 0.15.3 → 0.15.5
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/archive/index.d.ts +10 -0
- package/dist/archive/listArchive.d.ts +12 -0
- package/dist/archive/prune.d.ts +14 -0
- package/dist/archive/readArchive.d.ts +30 -0
- package/dist/archive/readLatest.d.ts +13 -0
- package/dist/archive/snapshot.d.ts +17 -0
- package/dist/bootstrap.d.ts +15 -0
- package/dist/cache.d.ts +22 -0
- package/dist/cli/jeeves-meta/architect.md +17 -0
- package/dist/cli/jeeves-meta/index.js +811 -734
- package/dist/cli.d.ts +10 -0
- package/dist/configHotReload.d.ts +30 -0
- package/dist/configLoader.d.ts +37 -0
- package/dist/constants.d.ts +13 -0
- package/dist/customCliCommands.d.ts +13 -0
- package/dist/descriptor.d.ts +19 -0
- package/dist/discovery/buildMinimalNode.d.ts +22 -0
- package/dist/discovery/computeSummary.d.ts +17 -0
- package/dist/discovery/discoverMetas.d.ts +19 -0
- package/dist/discovery/index.d.ts +11 -0
- package/dist/discovery/listMetas.d.ts +63 -0
- package/dist/discovery/ownershipTree.d.ts +25 -0
- package/dist/discovery/scope.d.ts +47 -0
- package/dist/discovery/types.d.ts +25 -0
- package/dist/ema.d.ts +14 -0
- package/dist/errors.d.ts +15 -0
- package/dist/escapeGlob.d.ts +23 -0
- package/dist/executor/GatewayExecutor.d.ts +48 -0
- package/dist/executor/SpawnAbortedError.d.ts +9 -0
- package/dist/executor/SpawnTimeoutError.d.ts +13 -0
- package/dist/executor/index.d.ts +8 -0
- package/dist/index.d.ts +34 -1660
- package/dist/index.js +1434 -1767
- package/dist/interfaces/MetaContext.d.ts +36 -0
- package/dist/interfaces/MetaExecutor.d.ts +46 -0
- package/dist/interfaces/WatcherClient.d.ts +75 -0
- package/dist/interfaces/index.d.ts +8 -0
- package/dist/lock.d.ts +70 -0
- package/dist/logger/index.d.ts +27 -0
- package/dist/mtimeFilter.d.ts +26 -0
- package/dist/normalizePath.d.ts +6 -0
- package/dist/orchestrator/buildTask.d.ts +38 -0
- package/dist/orchestrator/contextPackage.d.ts +30 -0
- package/dist/orchestrator/index.d.ts +10 -0
- package/dist/orchestrator/orchestratePhase.d.ts +38 -0
- package/dist/orchestrator/parseOutput.d.ts +41 -0
- package/dist/orchestrator/runPhase.d.ts +40 -0
- package/dist/phaseState/derivePhaseState.d.ts +41 -0
- package/dist/phaseState/index.d.ts +9 -0
- package/dist/phaseState/invalidate.d.ts +41 -0
- package/dist/phaseState/phaseScheduler.d.ts +57 -0
- package/dist/phaseState/phaseTransitions.d.ts +83 -0
- package/dist/progress/index.d.ts +38 -0
- package/dist/prompts/architect.md +17 -0
- package/dist/prompts/index.d.ts +15 -0
- package/dist/queue/index.d.ts +131 -0
- package/dist/readMetaJson.d.ts +17 -0
- package/dist/routes/__testUtils.d.ts +37 -0
- package/dist/routes/config.d.ts +11 -0
- package/dist/routes/configApply.d.ts +13 -0
- package/dist/routes/index.d.ts +50 -0
- package/dist/routes/metas.d.ts +9 -0
- package/dist/routes/metasUpdate.d.ts +11 -0
- package/dist/routes/preview.d.ts +8 -0
- package/dist/routes/queue.d.ts +13 -0
- package/dist/routes/seed.d.ts +8 -0
- package/dist/routes/status.d.ts +13 -0
- package/dist/routes/synthesize.d.ts +12 -0
- package/dist/routes/unlock.d.ts +8 -0
- package/dist/rules/healthCheck.d.ts +36 -0
- package/dist/rules/index.d.ts +39 -0
- package/dist/rules/verify.d.ts +22 -0
- package/dist/scheduler/index.d.ts +66 -0
- package/dist/scheduling/index.d.ts +7 -0
- package/dist/scheduling/staleness.d.ts +68 -0
- package/dist/scheduling/weightedFormula.d.ts +38 -0
- package/dist/schema/config.d.ts +54 -0
- package/dist/schema/error.d.ts +6 -0
- package/dist/schema/index.d.ts +8 -0
- package/dist/schema/meta.d.ts +71 -0
- package/dist/seed/autoSeed.d.ts +30 -0
- package/dist/seed/createMeta.d.ts +38 -0
- package/dist/seed/index.d.ts +7 -0
- package/dist/server.d.ts +24 -0
- package/dist/shutdown/index.d.ts +33 -0
- package/dist/structureHash.d.ts +15 -0
- package/dist/watcher-client/HttpWatcherClient.d.ts +38 -0
- package/dist/watcher-client/index.d.ts +6 -0
- package/package.json +17 -27
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import fs, { readdirSync, readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, mkdirSync, copyFileSync,
|
|
1
|
+
import fs, { readdirSync, readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, statSync, mkdirSync, copyFileSync, watchFile } from 'node:fs';
|
|
2
2
|
import path, { join, dirname, resolve, basename, relative, posix } from 'node:path';
|
|
3
3
|
import { unlink, readFile, mkdir, writeFile, copyFile } from 'node:fs/promises';
|
|
4
|
+
import { z } from 'zod';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import process$1 from 'node:process';
|
|
6
7
|
import { randomUUID, createHash } from 'node:crypto';
|
|
@@ -12,7 +13,6 @@ import require$$4 from 'util';
|
|
|
12
13
|
import require$$5 from 'assert';
|
|
13
14
|
import require$$2 from 'events';
|
|
14
15
|
import vm from 'vm';
|
|
15
|
-
import { z } from 'zod';
|
|
16
16
|
import * as commander from 'commander';
|
|
17
17
|
import 'node:child_process';
|
|
18
18
|
import { tmpdir } from 'node:os';
|
|
@@ -67,6 +67,72 @@ async function pruneArchive(metaPath, maxArchive) {
|
|
|
67
67
|
return toRemove;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Shared component descriptor constants for jeeves-meta.
|
|
72
|
+
*
|
|
73
|
+
* Single source of truth consumed by both the service descriptor and
|
|
74
|
+
* the OpenClaw plugin registration.
|
|
75
|
+
*/
|
|
76
|
+
/** Shared jeeves-meta component descriptor constants. */
|
|
77
|
+
const META_COMPONENT = {
|
|
78
|
+
name: 'meta',
|
|
79
|
+
servicePackage: '@karmaniverous/jeeves-meta',
|
|
80
|
+
pluginPackage: '@karmaniverous/jeeves-meta-openclaw',
|
|
81
|
+
defaultPort: 1938};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Structured error schema from a synthesis step failure.
|
|
85
|
+
*
|
|
86
|
+
*/
|
|
87
|
+
/** Zod schema for synthesis step errors. */
|
|
88
|
+
const metaErrorSchema = z.object({
|
|
89
|
+
/** Which step failed: 'architect', 'builder', or 'critic'. */
|
|
90
|
+
step: z.enum(['architect', 'builder', 'critic']),
|
|
91
|
+
/** Error classification code. */
|
|
92
|
+
code: z.string(),
|
|
93
|
+
/** Human-readable error message. */
|
|
94
|
+
message: z.string(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/** Zod schema for the core (library-compatible) meta configuration. */
|
|
98
|
+
/** Zod schema for the core (library-compatible) meta configuration. */
|
|
99
|
+
const metaConfigSchema = z.object({
|
|
100
|
+
/** Watcher service base URL. */
|
|
101
|
+
watcherUrl: z.url(),
|
|
102
|
+
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
103
|
+
gatewayUrl: z.url().default('http://127.0.0.1:18789'),
|
|
104
|
+
/** Optional API key for gateway authentication. */
|
|
105
|
+
gatewayApiKey: z.string().optional(),
|
|
106
|
+
/** Run architect every N cycles (per meta). */
|
|
107
|
+
architectEvery: z.number().int().min(1).default(10),
|
|
108
|
+
/** Exponent for depth weighting in staleness formula. */
|
|
109
|
+
depthWeight: z.number().min(0).default(0.5),
|
|
110
|
+
/** Maximum archive snapshots to retain per meta. */
|
|
111
|
+
maxArchive: z.number().int().min(1).default(20),
|
|
112
|
+
/** Maximum lines of context to include in subprocess prompts. */
|
|
113
|
+
maxLines: z.number().int().min(50).default(500),
|
|
114
|
+
/** Architect subprocess timeout in seconds. */
|
|
115
|
+
architectTimeout: z.number().int().min(30).default(180),
|
|
116
|
+
/** Builder subprocess timeout in seconds. */
|
|
117
|
+
builderTimeout: z.number().int().min(60).default(360),
|
|
118
|
+
/** Critic subprocess timeout in seconds. */
|
|
119
|
+
criticTimeout: z.number().int().min(30).default(240),
|
|
120
|
+
/** Thinking level for spawned synthesis sessions. */
|
|
121
|
+
thinking: z.string().default('low'),
|
|
122
|
+
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
123
|
+
defaultArchitect: z.string().optional(),
|
|
124
|
+
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
125
|
+
defaultCritic: z.string().optional(),
|
|
126
|
+
/** Skip unchanged candidates, bump _generatedAt. */
|
|
127
|
+
skipUnchanged: z.boolean().default(true),
|
|
128
|
+
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
129
|
+
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
130
|
+
/** Watcher metadata properties applied to archive snapshots. */
|
|
131
|
+
metaArchiveProperty: z
|
|
132
|
+
.record(z.string(), z.unknown())
|
|
133
|
+
.default({ _meta: 'archive' }),
|
|
134
|
+
});
|
|
135
|
+
|
|
70
136
|
/**
|
|
71
137
|
* Normalize file paths to forward slashes for consistency with watcher-indexed paths.
|
|
72
138
|
*
|
|
@@ -74,7 +140,6 @@ async function pruneArchive(metaPath, maxArchive) {
|
|
|
74
140
|
* ensures all paths in the library use the same convention, regardless of
|
|
75
141
|
* the platform's native separator.
|
|
76
142
|
*
|
|
77
|
-
* @module normalizePath
|
|
78
143
|
*/
|
|
79
144
|
/**
|
|
80
145
|
* Normalize a file path to forward slashes.
|
|
@@ -85,6 +150,22 @@ async function pruneArchive(metaPath, maxArchive) {
|
|
|
85
150
|
function normalizePath(p) {
|
|
86
151
|
return p.replaceAll('\\', '/');
|
|
87
152
|
}
|
|
153
|
+
/** Valid states for a synthesis phase. */
|
|
154
|
+
const phaseStatuses = [
|
|
155
|
+
'fresh',
|
|
156
|
+
'stale',
|
|
157
|
+
'pending',
|
|
158
|
+
'running',
|
|
159
|
+
'failed',
|
|
160
|
+
];
|
|
161
|
+
/** Zod schema for a per-phase status value. */
|
|
162
|
+
const phaseStatusSchema = z.enum(phaseStatuses);
|
|
163
|
+
/** Zod schema for the per-meta phase state record. */
|
|
164
|
+
const phaseStateSchema = z.object({
|
|
165
|
+
architect: phaseStatusSchema,
|
|
166
|
+
builder: phaseStatusSchema,
|
|
167
|
+
critic: phaseStatusSchema,
|
|
168
|
+
});
|
|
88
169
|
|
|
89
170
|
/**
|
|
90
171
|
* Archive reading helpers — watcher scan with filesystem fallback.
|
|
@@ -322,7 +403,7 @@ function packageDirectorySync({cwd, ignoreTypeOnlyPackageJson} = {}) {
|
|
|
322
403
|
* @module constants
|
|
323
404
|
*/
|
|
324
405
|
/** Default HTTP port for the jeeves-meta service. */
|
|
325
|
-
const DEFAULT_PORT =
|
|
406
|
+
const DEFAULT_PORT = META_COMPONENT.defaultPort;
|
|
326
407
|
/** Default port as a string (for Commander CLI defaults). */
|
|
327
408
|
const DEFAULT_PORT_STR = String(DEFAULT_PORT);
|
|
328
409
|
/** Service name identifier. */
|
|
@@ -1503,11 +1584,11 @@ var hasRequiredRetry$1;
|
|
|
1503
1584
|
function requireRetry$1 () {
|
|
1504
1585
|
if (hasRequiredRetry$1) return retry$1;
|
|
1505
1586
|
hasRequiredRetry$1 = 1;
|
|
1506
|
-
(function (exports
|
|
1587
|
+
(function (exports) {
|
|
1507
1588
|
var RetryOperation = requireRetry_operation();
|
|
1508
1589
|
|
|
1509
|
-
exports
|
|
1510
|
-
var timeouts = exports
|
|
1590
|
+
exports.operation = function(options) {
|
|
1591
|
+
var timeouts = exports.timeouts(options);
|
|
1511
1592
|
return new RetryOperation(timeouts, {
|
|
1512
1593
|
forever: options && options.forever,
|
|
1513
1594
|
unref: options && options.unref,
|
|
@@ -1515,7 +1596,7 @@ function requireRetry$1 () {
|
|
|
1515
1596
|
});
|
|
1516
1597
|
};
|
|
1517
1598
|
|
|
1518
|
-
exports
|
|
1599
|
+
exports.timeouts = function(options) {
|
|
1519
1600
|
if (options instanceof Array) {
|
|
1520
1601
|
return [].concat(options);
|
|
1521
1602
|
}
|
|
@@ -1552,7 +1633,7 @@ function requireRetry$1 () {
|
|
|
1552
1633
|
return timeouts;
|
|
1553
1634
|
};
|
|
1554
1635
|
|
|
1555
|
-
exports
|
|
1636
|
+
exports.createTimeout = function(attempt, opts) {
|
|
1556
1637
|
var random = (opts.randomize)
|
|
1557
1638
|
? (Math.random() + 1)
|
|
1558
1639
|
: 1;
|
|
@@ -1563,7 +1644,7 @@ function requireRetry$1 () {
|
|
|
1563
1644
|
return timeout;
|
|
1564
1645
|
};
|
|
1565
1646
|
|
|
1566
|
-
exports
|
|
1647
|
+
exports.wrap = function(obj, options, methods) {
|
|
1567
1648
|
if (options instanceof Array) {
|
|
1568
1649
|
methods = options;
|
|
1569
1650
|
options = null;
|
|
@@ -1583,7 +1664,7 @@ function requireRetry$1 () {
|
|
|
1583
1664
|
var original = obj[method];
|
|
1584
1665
|
|
|
1585
1666
|
obj[method] = function retryWrapper(original) {
|
|
1586
|
-
var op = exports
|
|
1667
|
+
var op = exports.operation(options);
|
|
1587
1668
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
1588
1669
|
var callback = args.pop();
|
|
1589
1670
|
|
|
@@ -4592,7 +4673,7 @@ var hasRequiredRe;
|
|
|
4592
4673
|
function requireRe () {
|
|
4593
4674
|
if (hasRequiredRe) return re.exports;
|
|
4594
4675
|
hasRequiredRe = 1;
|
|
4595
|
-
(function (module, exports
|
|
4676
|
+
(function (module, exports) {
|
|
4596
4677
|
|
|
4597
4678
|
const {
|
|
4598
4679
|
MAX_SAFE_COMPONENT_LENGTH,
|
|
@@ -4600,14 +4681,14 @@ function requireRe () {
|
|
|
4600
4681
|
MAX_LENGTH,
|
|
4601
4682
|
} = requireConstants();
|
|
4602
4683
|
const debug = requireDebug();
|
|
4603
|
-
exports
|
|
4684
|
+
exports = module.exports = {};
|
|
4604
4685
|
|
|
4605
4686
|
// The actual regexps go on exports.re
|
|
4606
|
-
const re = exports
|
|
4607
|
-
const safeRe = exports
|
|
4608
|
-
const src = exports
|
|
4609
|
-
const safeSrc = exports
|
|
4610
|
-
const t = exports
|
|
4687
|
+
const re = exports.re = [];
|
|
4688
|
+
const safeRe = exports.safeRe = [];
|
|
4689
|
+
const src = exports.src = [];
|
|
4690
|
+
const safeSrc = exports.safeSrc = [];
|
|
4691
|
+
const t = exports.t = {};
|
|
4611
4692
|
let R = 0;
|
|
4612
4693
|
|
|
4613
4694
|
const LETTERDASHNUMBER = '[a-zA-Z0-9-]';
|
|
@@ -4730,7 +4811,7 @@ function requireRe () {
|
|
|
4730
4811
|
createToken('GTLT', '((?:<|>)?=?)');
|
|
4731
4812
|
|
|
4732
4813
|
// Something like "2.*" or "1.2.x".
|
|
4733
|
-
// Note that "x.x" is a valid xRange
|
|
4814
|
+
// Note that "x.x" is a valid xRange identifier, meaning "any version"
|
|
4734
4815
|
// Only the first item is strictly required.
|
|
4735
4816
|
createToken('XRANGEIDENTIFIERLOOSE', `${src[t.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`);
|
|
4736
4817
|
createToken('XRANGEIDENTIFIER', `${src[t.NUMERICIDENTIFIER]}|x|X|\\*`);
|
|
@@ -4771,7 +4852,7 @@ function requireRe () {
|
|
|
4771
4852
|
createToken('LONETILDE', '(?:~>?)');
|
|
4772
4853
|
|
|
4773
4854
|
createToken('TILDETRIM', `(\\s*)${src[t.LONETILDE]}\\s+`, true);
|
|
4774
|
-
exports
|
|
4855
|
+
exports.tildeTrimReplace = '$1~';
|
|
4775
4856
|
|
|
4776
4857
|
createToken('TILDE', `^${src[t.LONETILDE]}${src[t.XRANGEPLAIN]}$`);
|
|
4777
4858
|
createToken('TILDELOOSE', `^${src[t.LONETILDE]}${src[t.XRANGEPLAINLOOSE]}$`);
|
|
@@ -4781,7 +4862,7 @@ function requireRe () {
|
|
|
4781
4862
|
createToken('LONECARET', '(?:\\^)');
|
|
4782
4863
|
|
|
4783
4864
|
createToken('CARETTRIM', `(\\s*)${src[t.LONECARET]}\\s+`, true);
|
|
4784
|
-
exports
|
|
4865
|
+
exports.caretTrimReplace = '$1^';
|
|
4785
4866
|
|
|
4786
4867
|
createToken('CARET', `^${src[t.LONECARET]}${src[t.XRANGEPLAIN]}$`);
|
|
4787
4868
|
createToken('CARETLOOSE', `^${src[t.LONECARET]}${src[t.XRANGEPLAINLOOSE]}$`);
|
|
@@ -4794,7 +4875,7 @@ function requireRe () {
|
|
|
4794
4875
|
// it modifies, so that `> 1.2.3` ==> `>1.2.3`
|
|
4795
4876
|
createToken('COMPARATORTRIM', `(\\s*)${src[t.GTLT]
|
|
4796
4877
|
}\\s*(${src[t.LOOSEPLAIN]}|${src[t.XRANGEPLAIN]})`, true);
|
|
4797
|
-
exports
|
|
4878
|
+
exports.comparatorTrimReplace = '$1$2$3';
|
|
4798
4879
|
|
|
4799
4880
|
// Something like `1.2.3 - 1.2.4`
|
|
4800
4881
|
// Note that these all use the loose form, because they'll be
|
|
@@ -5726,6 +5807,62 @@ function requireCoerce () {
|
|
|
5726
5807
|
return coerce_1;
|
|
5727
5808
|
}
|
|
5728
5809
|
|
|
5810
|
+
var truncate_1;
|
|
5811
|
+
var hasRequiredTruncate;
|
|
5812
|
+
|
|
5813
|
+
function requireTruncate () {
|
|
5814
|
+
if (hasRequiredTruncate) return truncate_1;
|
|
5815
|
+
hasRequiredTruncate = 1;
|
|
5816
|
+
|
|
5817
|
+
const parse = requireParse();
|
|
5818
|
+
const constants = requireConstants();
|
|
5819
|
+
const SemVer = requireSemver$1();
|
|
5820
|
+
|
|
5821
|
+
const truncate = (version, truncation, options) => {
|
|
5822
|
+
if (!constants.RELEASE_TYPES.includes(truncation)) {
|
|
5823
|
+
return null
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
const clonedVersion = cloneInputVersion(version, options);
|
|
5827
|
+
return clonedVersion && doTruncation(clonedVersion, truncation)
|
|
5828
|
+
};
|
|
5829
|
+
|
|
5830
|
+
const cloneInputVersion = (version, options) => {
|
|
5831
|
+
const versionStringToParse = (
|
|
5832
|
+
version instanceof SemVer ? version.version : version
|
|
5833
|
+
);
|
|
5834
|
+
|
|
5835
|
+
return parse(versionStringToParse, options)
|
|
5836
|
+
};
|
|
5837
|
+
|
|
5838
|
+
const doTruncation = (version, truncation) => {
|
|
5839
|
+
if (isPrerelease(truncation)) {
|
|
5840
|
+
return version.version
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
version.prerelease = [];
|
|
5844
|
+
|
|
5845
|
+
switch (truncation) {
|
|
5846
|
+
case 'major':
|
|
5847
|
+
version.minor = 0;
|
|
5848
|
+
version.patch = 0;
|
|
5849
|
+
break
|
|
5850
|
+
case 'minor':
|
|
5851
|
+
version.patch = 0;
|
|
5852
|
+
break
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5855
|
+
return version.format()
|
|
5856
|
+
};
|
|
5857
|
+
|
|
5858
|
+
const isPrerelease = (type) => {
|
|
5859
|
+
return type.startsWith('pre')
|
|
5860
|
+
};
|
|
5861
|
+
|
|
5862
|
+
truncate_1 = truncate;
|
|
5863
|
+
return truncate_1;
|
|
5864
|
+
}
|
|
5865
|
+
|
|
5729
5866
|
var lrucache;
|
|
5730
5867
|
var hasRequiredLrucache;
|
|
5731
5868
|
|
|
@@ -7175,6 +7312,7 @@ function requireSemver () {
|
|
|
7175
7312
|
const lte = requireLte();
|
|
7176
7313
|
const cmp = requireCmp();
|
|
7177
7314
|
const coerce = requireCoerce();
|
|
7315
|
+
const truncate = requireTruncate();
|
|
7178
7316
|
const Comparator = requireComparator();
|
|
7179
7317
|
const Range = requireRange();
|
|
7180
7318
|
const satisfies = requireSatisfies();
|
|
@@ -7213,6 +7351,7 @@ function requireSemver () {
|
|
|
7213
7351
|
lte,
|
|
7214
7352
|
cmp,
|
|
7215
7353
|
coerce,
|
|
7354
|
+
truncate,
|
|
7216
7355
|
Comparator,
|
|
7217
7356
|
Range,
|
|
7218
7357
|
satisfies,
|
|
@@ -7510,31 +7649,31 @@ var hasRequiredExtraTypings;
|
|
|
7510
7649
|
function requireExtraTypings () {
|
|
7511
7650
|
if (hasRequiredExtraTypings) return extraTypings.exports;
|
|
7512
7651
|
hasRequiredExtraTypings = 1;
|
|
7513
|
-
(function (module, exports
|
|
7652
|
+
(function (module, exports) {
|
|
7514
7653
|
const commander = require$$0;
|
|
7515
7654
|
|
|
7516
|
-
exports
|
|
7655
|
+
exports = module.exports = {};
|
|
7517
7656
|
|
|
7518
7657
|
// Return a different global program than commander,
|
|
7519
7658
|
// and don't also return it as default export.
|
|
7520
|
-
exports
|
|
7659
|
+
exports.program = new commander.Command();
|
|
7521
7660
|
|
|
7522
7661
|
/**
|
|
7523
7662
|
* Expose classes. The FooT versions are just types, so return Commander original implementations!
|
|
7524
7663
|
*/
|
|
7525
7664
|
|
|
7526
|
-
exports
|
|
7527
|
-
exports
|
|
7528
|
-
exports
|
|
7529
|
-
exports
|
|
7530
|
-
exports
|
|
7531
|
-
exports
|
|
7532
|
-
exports
|
|
7665
|
+
exports.Argument = commander.Argument;
|
|
7666
|
+
exports.Command = commander.Command;
|
|
7667
|
+
exports.CommanderError = commander.CommanderError;
|
|
7668
|
+
exports.Help = commander.Help;
|
|
7669
|
+
exports.InvalidArgumentError = commander.InvalidArgumentError;
|
|
7670
|
+
exports.InvalidOptionArgumentError = commander.InvalidArgumentError; // Deprecated
|
|
7671
|
+
exports.Option = commander.Option;
|
|
7533
7672
|
|
|
7534
|
-
exports
|
|
7535
|
-
exports
|
|
7673
|
+
exports.createCommand = (name) => new commander.Command(name);
|
|
7674
|
+
exports.createOption = (flags, description) =>
|
|
7536
7675
|
new commander.Option(flags, description);
|
|
7537
|
-
exports
|
|
7676
|
+
exports.createArgument = (name, description) =>
|
|
7538
7677
|
new commander.Argument(name, description);
|
|
7539
7678
|
} (extraTypings, extraTypings.exports));
|
|
7540
7679
|
return extraTypings.exports;
|
|
@@ -8116,508 +8255,247 @@ function registerCustomCliCommands(program) {
|
|
|
8116
8255
|
}
|
|
8117
8256
|
|
|
8118
8257
|
/**
|
|
8119
|
-
*
|
|
8258
|
+
* Compute summary statistics from an array of MetaEntry objects.
|
|
8120
8259
|
*
|
|
8121
|
-
*
|
|
8122
|
-
* via the component descriptor's onConfigApply callback.
|
|
8260
|
+
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
8123
8261
|
*
|
|
8124
|
-
* @module
|
|
8262
|
+
* @module discovery/computeSummary
|
|
8125
8263
|
*/
|
|
8126
8264
|
/**
|
|
8127
|
-
*
|
|
8265
|
+
* Compute summary statistics from a list of meta entries.
|
|
8128
8266
|
*
|
|
8129
|
-
*
|
|
8130
|
-
*
|
|
8267
|
+
* @param entries - Enriched meta entries (full or filtered).
|
|
8268
|
+
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
8269
|
+
* @returns Aggregated summary statistics.
|
|
8131
8270
|
*/
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
let
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8271
|
+
function computeSummary(entries, depthWeight) {
|
|
8272
|
+
let staleCount = 0;
|
|
8273
|
+
let errorCount = 0;
|
|
8274
|
+
let lockedCount = 0;
|
|
8275
|
+
let disabledCount = 0;
|
|
8276
|
+
let neverSynthesizedCount = 0;
|
|
8277
|
+
let totalArchitectTokens = 0;
|
|
8278
|
+
let totalBuilderTokens = 0;
|
|
8279
|
+
let totalCriticTokens = 0;
|
|
8280
|
+
let stalestPath = null;
|
|
8281
|
+
let stalestEffective = -1;
|
|
8282
|
+
let lastSynthesizedPath = null;
|
|
8283
|
+
let lastSynthesizedAt = null;
|
|
8284
|
+
for (const e of entries) {
|
|
8285
|
+
if (e.stalenessSeconds > 0)
|
|
8286
|
+
staleCount++;
|
|
8287
|
+
if (e.hasError)
|
|
8288
|
+
errorCount++;
|
|
8289
|
+
if (e.locked)
|
|
8290
|
+
lockedCount++;
|
|
8291
|
+
if (e.disabled)
|
|
8292
|
+
disabledCount++;
|
|
8293
|
+
if (e.lastSynthesized === null)
|
|
8294
|
+
neverSynthesizedCount++;
|
|
8295
|
+
totalArchitectTokens += e.architectTokens ?? 0;
|
|
8296
|
+
totalBuilderTokens += e.builderTokens ?? 0;
|
|
8297
|
+
totalCriticTokens += e.criticTokens ?? 0;
|
|
8298
|
+
// Track last synthesized
|
|
8299
|
+
if (e.lastSynthesized &&
|
|
8300
|
+
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
8301
|
+
lastSynthesizedAt = e.lastSynthesized;
|
|
8302
|
+
lastSynthesizedPath = e.path;
|
|
8303
|
+
}
|
|
8304
|
+
// Track stalest (effective staleness for scheduling)
|
|
8305
|
+
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
8306
|
+
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
8307
|
+
if (effectiveStaleness > stalestEffective) {
|
|
8308
|
+
stalestEffective = effectiveStaleness;
|
|
8309
|
+
stalestPath = e.path;
|
|
8155
8310
|
}
|
|
8156
8311
|
}
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8312
|
+
return {
|
|
8313
|
+
total: entries.length,
|
|
8314
|
+
stale: staleCount,
|
|
8315
|
+
errors: errorCount,
|
|
8316
|
+
locked: lockedCount,
|
|
8317
|
+
disabled: disabledCount,
|
|
8318
|
+
neverSynthesized: neverSynthesizedCount,
|
|
8319
|
+
tokens: {
|
|
8320
|
+
architect: totalArchitectTokens,
|
|
8321
|
+
builder: totalBuilderTokens,
|
|
8322
|
+
critic: totalCriticTokens,
|
|
8323
|
+
},
|
|
8324
|
+
stalestPath,
|
|
8325
|
+
lastSynthesizedPath,
|
|
8326
|
+
lastSynthesizedAt,
|
|
8327
|
+
};
|
|
8328
|
+
}
|
|
8329
|
+
|
|
8330
|
+
/**
|
|
8331
|
+
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
8332
|
+
*
|
|
8333
|
+
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
8334
|
+
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
8335
|
+
*
|
|
8336
|
+
* @module discovery/discoverMetas
|
|
8337
|
+
*/
|
|
8338
|
+
/**
|
|
8339
|
+
* Discover all .meta/ directories via watcher walk.
|
|
8340
|
+
*
|
|
8341
|
+
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
8342
|
+
* and returns deduplicated meta directory paths.
|
|
8343
|
+
*
|
|
8344
|
+
* @param watcher - WatcherClient for walk queries.
|
|
8345
|
+
* @returns Array of normalized .meta/ directory paths.
|
|
8346
|
+
*/
|
|
8347
|
+
async function discoverMetas(watcher) {
|
|
8348
|
+
const allPaths = await watcher.walk(['**/.meta/meta.json']);
|
|
8349
|
+
// Deduplicate by .meta/ directory path (handles multi-chunk files)
|
|
8350
|
+
const seen = new Set();
|
|
8351
|
+
const metaPaths = [];
|
|
8352
|
+
for (const filePath of allPaths) {
|
|
8353
|
+
const fp = normalizePath(filePath);
|
|
8354
|
+
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
8355
|
+
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
8356
|
+
if (seen.has(metaPath))
|
|
8170
8357
|
continue;
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
const nextVal = newConfig[key];
|
|
8174
|
-
if (JSON.stringify(oldVal) !== JSON.stringify(nextVal)) {
|
|
8175
|
-
config[key] = nextVal;
|
|
8176
|
-
logger.info({ field: key }, 'Config field hot-reloaded');
|
|
8177
|
-
}
|
|
8358
|
+
seen.add(metaPath);
|
|
8359
|
+
metaPaths.push(metaPath);
|
|
8178
8360
|
}
|
|
8361
|
+
return metaPaths;
|
|
8179
8362
|
}
|
|
8180
8363
|
|
|
8181
8364
|
/**
|
|
8182
|
-
*
|
|
8365
|
+
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
8183
8366
|
*
|
|
8184
|
-
*
|
|
8367
|
+
* Lock file: .meta/.lock containing `_lockPid` + `_lockStartedAt` (underscore-prefixed
|
|
8368
|
+
* reserved keys, consistent with meta.json conventions).
|
|
8369
|
+
* Stale timeout: 30 minutes.
|
|
8185
8370
|
*
|
|
8186
|
-
* @module
|
|
8371
|
+
* @module lock
|
|
8187
8372
|
*/
|
|
8188
|
-
|
|
8189
|
-
const metaConfigSchema = z.object({
|
|
8190
|
-
/** Watcher service base URL. */
|
|
8191
|
-
watcherUrl: z.url(),
|
|
8192
|
-
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
8193
|
-
gatewayUrl: z.url().default('http://127.0.0.1:18789'),
|
|
8194
|
-
/** Optional API key for gateway authentication. */
|
|
8195
|
-
gatewayApiKey: z.string().optional(),
|
|
8196
|
-
/** Run architect every N cycles (per meta). */
|
|
8197
|
-
architectEvery: z.number().int().min(1).default(10),
|
|
8198
|
-
/** Exponent for depth weighting in staleness formula. */
|
|
8199
|
-
depthWeight: z.number().min(0).default(0.5),
|
|
8200
|
-
/** Maximum archive snapshots to retain per meta. */
|
|
8201
|
-
maxArchive: z.number().int().min(1).default(20),
|
|
8202
|
-
/** Maximum lines of context to include in subprocess prompts. */
|
|
8203
|
-
maxLines: z.number().int().min(50).default(500),
|
|
8204
|
-
/** Architect subprocess timeout in seconds. */
|
|
8205
|
-
architectTimeout: z.number().int().min(30).default(180),
|
|
8206
|
-
/** Builder subprocess timeout in seconds. */
|
|
8207
|
-
builderTimeout: z.number().int().min(60).default(360),
|
|
8208
|
-
/** Critic subprocess timeout in seconds. */
|
|
8209
|
-
criticTimeout: z.number().int().min(30).default(240),
|
|
8210
|
-
/** Thinking level for spawned synthesis sessions. */
|
|
8211
|
-
thinking: z.string().default('low'),
|
|
8212
|
-
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
8213
|
-
defaultArchitect: z.string().optional(),
|
|
8214
|
-
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
8215
|
-
defaultCritic: z.string().optional(),
|
|
8216
|
-
/** Skip unchanged candidates, bump _generatedAt. */
|
|
8217
|
-
skipUnchanged: z.boolean().default(true),
|
|
8218
|
-
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
8219
|
-
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
8220
|
-
/** Watcher metadata properties applied to archive snapshots. */
|
|
8221
|
-
metaArchiveProperty: z
|
|
8222
|
-
.record(z.string(), z.unknown())
|
|
8223
|
-
.default({ _meta: 'archive' }),
|
|
8224
|
-
});
|
|
8225
|
-
/** Zod schema for logging configuration. */
|
|
8226
|
-
const loggingSchema = z.object({
|
|
8227
|
-
/** Log level. */
|
|
8228
|
-
level: z.string().default('info'),
|
|
8229
|
-
/** Optional file path for log output. */
|
|
8230
|
-
file: z.string().optional(),
|
|
8231
|
-
});
|
|
8232
|
-
/** Zod schema for a single auto-seed policy rule. */
|
|
8233
|
-
const autoSeedRuleSchema = z.object({
|
|
8234
|
-
/** Glob pattern matched against watcher walk results. */
|
|
8235
|
-
match: z.string(),
|
|
8236
|
-
/** Optional steering prompt for seeded metas. */
|
|
8237
|
-
steer: z.string().optional(),
|
|
8238
|
-
/** Optional cross-references for seeded metas. */
|
|
8239
|
-
crossRefs: z.array(z.string()).optional(),
|
|
8240
|
-
});
|
|
8241
|
-
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
8242
|
-
const serviceConfigSchema = metaConfigSchema.extend({
|
|
8243
|
-
/** HTTP port for the service (default: 1938). */
|
|
8244
|
-
port: z.number().int().min(1).max(65535).default(1938),
|
|
8245
|
-
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
8246
|
-
schedule: z.string().default('*/30 * * * *'),
|
|
8247
|
-
/** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
|
|
8248
|
-
reportChannel: z.string().optional(),
|
|
8249
|
-
/** Channel/user ID to send progress messages to. */
|
|
8250
|
-
reportTarget: z.string().optional(),
|
|
8251
|
-
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
8252
|
-
serverBaseUrl: z.string().optional(),
|
|
8253
|
-
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
8254
|
-
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
8255
|
-
/** Logging configuration. */
|
|
8256
|
-
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
8257
|
-
/**
|
|
8258
|
-
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
8259
|
-
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
8260
|
-
*/
|
|
8261
|
-
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
8262
|
-
});
|
|
8263
|
-
|
|
8373
|
+
const LOCK_FILE = '.lock';
|
|
8264
8374
|
/**
|
|
8265
|
-
*
|
|
8375
|
+
* Resolve a path to a .meta directory.
|
|
8266
8376
|
*
|
|
8267
|
-
*
|
|
8377
|
+
* If the path already ends with '.meta', returns it as-is.
|
|
8378
|
+
* Otherwise, appends '.meta' as a subdirectory.
|
|
8268
8379
|
*
|
|
8269
|
-
* @
|
|
8380
|
+
* @param inputPath - Path that may or may not end with '.meta'.
|
|
8381
|
+
* @returns The resolved .meta directory path.
|
|
8270
8382
|
*/
|
|
8383
|
+
function resolveMetaDir(inputPath) {
|
|
8384
|
+
return inputPath.endsWith('.meta') ? inputPath : join(inputPath, '.meta');
|
|
8385
|
+
}
|
|
8386
|
+
const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
8271
8387
|
/**
|
|
8272
|
-
*
|
|
8388
|
+
* Read and classify the state of a .meta/.lock file.
|
|
8273
8389
|
*
|
|
8274
|
-
* @param
|
|
8275
|
-
* @returns
|
|
8390
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
8391
|
+
* @returns Parsed lock state.
|
|
8276
8392
|
*/
|
|
8277
|
-
function
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
if (envVal === undefined) {
|
|
8282
|
-
throw new Error(`Environment variable ${name} is not set`);
|
|
8283
|
-
}
|
|
8284
|
-
return envVal;
|
|
8285
|
-
});
|
|
8286
|
-
}
|
|
8287
|
-
if (Array.isArray(value)) {
|
|
8288
|
-
return value.map(substituteEnvVars);
|
|
8393
|
+
function readLockState(metaPath) {
|
|
8394
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
8395
|
+
if (!existsSync(lockPath)) {
|
|
8396
|
+
return { exists: false, staged: false, active: false, data: null };
|
|
8289
8397
|
}
|
|
8290
|
-
|
|
8291
|
-
const
|
|
8292
|
-
|
|
8293
|
-
|
|
8398
|
+
try {
|
|
8399
|
+
const raw = readFileSync(lockPath, 'utf8');
|
|
8400
|
+
const data = JSON.parse(raw);
|
|
8401
|
+
if ('_id' in data) {
|
|
8402
|
+
return { exists: true, staged: true, active: false, data };
|
|
8294
8403
|
}
|
|
8295
|
-
|
|
8404
|
+
const startedAt = data._lockStartedAt;
|
|
8405
|
+
if (startedAt) {
|
|
8406
|
+
const lockAge = Date.now() - new Date(startedAt).getTime();
|
|
8407
|
+
return {
|
|
8408
|
+
exists: true,
|
|
8409
|
+
staged: false,
|
|
8410
|
+
active: lockAge < STALE_TIMEOUT_MS,
|
|
8411
|
+
data,
|
|
8412
|
+
};
|
|
8413
|
+
}
|
|
8414
|
+
return { exists: true, staged: false, active: false, data };
|
|
8415
|
+
}
|
|
8416
|
+
catch {
|
|
8417
|
+
return { exists: true, staged: false, active: false, data: null };
|
|
8296
8418
|
}
|
|
8297
|
-
return value;
|
|
8298
8419
|
}
|
|
8299
8420
|
/**
|
|
8300
|
-
*
|
|
8421
|
+
* Attempt to acquire a lock on a .meta directory.
|
|
8301
8422
|
*
|
|
8302
|
-
* @param
|
|
8303
|
-
* @
|
|
8304
|
-
* @returns The resolved string (file contents or original value).
|
|
8423
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
8424
|
+
* @returns True if lock was acquired, false if already locked (non-stale).
|
|
8305
8425
|
*/
|
|
8306
|
-
function
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8426
|
+
function acquireLock(metaPath) {
|
|
8427
|
+
const state = readLockState(metaPath);
|
|
8428
|
+
// Active non-stale lock — cannot acquire
|
|
8429
|
+
if (state.active)
|
|
8430
|
+
return false;
|
|
8431
|
+
// Staged, stale, corrupt, or missing — safe to (over)write
|
|
8432
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
8433
|
+
const lock = {
|
|
8434
|
+
_lockPid: process.pid,
|
|
8435
|
+
_lockStartedAt: new Date().toISOString(),
|
|
8436
|
+
};
|
|
8437
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
8438
|
+
return true;
|
|
8311
8439
|
}
|
|
8312
8440
|
/**
|
|
8313
|
-
*
|
|
8314
|
-
*
|
|
8315
|
-
* If the old path `{configRoot}/jeeves-meta.config.json` exists and the new
|
|
8316
|
-
* path `{configRoot}/jeeves-meta/config.json` does NOT exist, copies the file
|
|
8317
|
-
* to the new location and logs a warning.
|
|
8441
|
+
* Release a lock on a .meta directory.
|
|
8318
8442
|
*
|
|
8319
|
-
* @param
|
|
8320
|
-
* @param warn - Optional callback for logging the migration warning.
|
|
8443
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
8321
8444
|
*/
|
|
8322
|
-
function
|
|
8323
|
-
const
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
const message = `Migrated config from ${oldPath} to ${newPath}. The old file can be removed.`;
|
|
8330
|
-
if (warn) {
|
|
8331
|
-
warn(message);
|
|
8332
|
-
}
|
|
8333
|
-
else {
|
|
8334
|
-
console.warn(`[jeeves-meta] ${message}`);
|
|
8335
|
-
}
|
|
8445
|
+
function releaseLock(metaPath) {
|
|
8446
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
8447
|
+
try {
|
|
8448
|
+
unlinkSync(lockPath);
|
|
8449
|
+
}
|
|
8450
|
+
catch {
|
|
8451
|
+
// Already removed or never existed
|
|
8336
8452
|
}
|
|
8337
8453
|
}
|
|
8338
8454
|
/**
|
|
8339
|
-
*
|
|
8455
|
+
* Check if a .meta directory is currently locked (non-stale).
|
|
8340
8456
|
*
|
|
8341
|
-
* @param
|
|
8342
|
-
* @returns
|
|
8343
|
-
* @throws If no config path found.
|
|
8457
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
8458
|
+
* @returns True if locked and not stale.
|
|
8344
8459
|
*/
|
|
8345
|
-
function
|
|
8346
|
-
|
|
8347
|
-
if (configIdx === -1)
|
|
8348
|
-
configIdx = args.indexOf('-c');
|
|
8349
|
-
if (configIdx !== -1 && args[configIdx + 1]) {
|
|
8350
|
-
return args[configIdx + 1];
|
|
8351
|
-
}
|
|
8352
|
-
const envPath = process.env['JEEVES_META_CONFIG'];
|
|
8353
|
-
if (envPath)
|
|
8354
|
-
return envPath;
|
|
8355
|
-
throw new Error('Config path required. Use --config <path> or set JEEVES_META_CONFIG env var.');
|
|
8460
|
+
function isLocked(metaPath) {
|
|
8461
|
+
return readLockState(metaPath).active;
|
|
8356
8462
|
}
|
|
8357
8463
|
/**
|
|
8358
|
-
*
|
|
8464
|
+
* Clean up stale lock files on startup.
|
|
8359
8465
|
*
|
|
8360
|
-
*
|
|
8361
|
-
*
|
|
8466
|
+
* For each .meta directory found via the provided paths:
|
|
8467
|
+
* - If lock contains PID-only data (synthesis incomplete), delete it.
|
|
8468
|
+
* - If lock contains staged result (_id present), log warning and delete.
|
|
8362
8469
|
*
|
|
8363
|
-
* @param
|
|
8364
|
-
* @
|
|
8470
|
+
* @param metaPaths - Array of .meta directory paths to check.
|
|
8471
|
+
* @param logger - Optional logger for warnings.
|
|
8365
8472
|
*/
|
|
8366
|
-
function
|
|
8367
|
-
const
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8473
|
+
function cleanupStaleLocks(metaPaths, logger) {
|
|
8474
|
+
for (const metaPath of metaPaths) {
|
|
8475
|
+
const state = readLockState(metaPath);
|
|
8476
|
+
if (!state.exists)
|
|
8477
|
+
continue;
|
|
8478
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
8479
|
+
if (state.staged) {
|
|
8480
|
+
logger?.warn({ metaPath }, 'Found staged synthesis result in lock file from previous crash — deleting (conservative: not auto-finalizing)');
|
|
8481
|
+
}
|
|
8482
|
+
else {
|
|
8483
|
+
logger?.warn({ metaPath }, 'Found stale lock file from previous crash — deleting');
|
|
8484
|
+
}
|
|
8485
|
+
try {
|
|
8486
|
+
unlinkSync(lockPath);
|
|
8487
|
+
}
|
|
8488
|
+
catch {
|
|
8489
|
+
// Already gone
|
|
8490
|
+
}
|
|
8375
8491
|
}
|
|
8376
|
-
return serviceConfigSchema.parse(raw);
|
|
8377
8492
|
}
|
|
8378
8493
|
|
|
8379
8494
|
/**
|
|
8380
|
-
*
|
|
8495
|
+
* Read and parse a meta.json file from a `.meta/` directory.
|
|
8381
8496
|
*
|
|
8382
|
-
* Shared
|
|
8383
|
-
*
|
|
8384
|
-
* @module discovery/computeSummary
|
|
8385
|
-
*/
|
|
8386
|
-
/**
|
|
8387
|
-
* Compute summary statistics from a list of meta entries.
|
|
8388
|
-
*
|
|
8389
|
-
* @param entries - Enriched meta entries (full or filtered).
|
|
8390
|
-
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
8391
|
-
* @returns Aggregated summary statistics.
|
|
8392
|
-
*/
|
|
8393
|
-
function computeSummary(entries, depthWeight) {
|
|
8394
|
-
let staleCount = 0;
|
|
8395
|
-
let errorCount = 0;
|
|
8396
|
-
let lockedCount = 0;
|
|
8397
|
-
let disabledCount = 0;
|
|
8398
|
-
let neverSynthesizedCount = 0;
|
|
8399
|
-
let totalArchitectTokens = 0;
|
|
8400
|
-
let totalBuilderTokens = 0;
|
|
8401
|
-
let totalCriticTokens = 0;
|
|
8402
|
-
let stalestPath = null;
|
|
8403
|
-
let stalestEffective = -1;
|
|
8404
|
-
let lastSynthesizedPath = null;
|
|
8405
|
-
let lastSynthesizedAt = null;
|
|
8406
|
-
for (const e of entries) {
|
|
8407
|
-
if (e.stalenessSeconds > 0)
|
|
8408
|
-
staleCount++;
|
|
8409
|
-
if (e.hasError)
|
|
8410
|
-
errorCount++;
|
|
8411
|
-
if (e.locked)
|
|
8412
|
-
lockedCount++;
|
|
8413
|
-
if (e.disabled)
|
|
8414
|
-
disabledCount++;
|
|
8415
|
-
if (e.lastSynthesized === null)
|
|
8416
|
-
neverSynthesizedCount++;
|
|
8417
|
-
totalArchitectTokens += e.architectTokens ?? 0;
|
|
8418
|
-
totalBuilderTokens += e.builderTokens ?? 0;
|
|
8419
|
-
totalCriticTokens += e.criticTokens ?? 0;
|
|
8420
|
-
// Track last synthesized
|
|
8421
|
-
if (e.lastSynthesized &&
|
|
8422
|
-
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
8423
|
-
lastSynthesizedAt = e.lastSynthesized;
|
|
8424
|
-
lastSynthesizedPath = e.path;
|
|
8425
|
-
}
|
|
8426
|
-
// Track stalest (effective staleness for scheduling)
|
|
8427
|
-
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
8428
|
-
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
8429
|
-
if (effectiveStaleness > stalestEffective) {
|
|
8430
|
-
stalestEffective = effectiveStaleness;
|
|
8431
|
-
stalestPath = e.path;
|
|
8432
|
-
}
|
|
8433
|
-
}
|
|
8434
|
-
return {
|
|
8435
|
-
total: entries.length,
|
|
8436
|
-
stale: staleCount,
|
|
8437
|
-
errors: errorCount,
|
|
8438
|
-
locked: lockedCount,
|
|
8439
|
-
disabled: disabledCount,
|
|
8440
|
-
neverSynthesized: neverSynthesizedCount,
|
|
8441
|
-
tokens: {
|
|
8442
|
-
architect: totalArchitectTokens,
|
|
8443
|
-
builder: totalBuilderTokens,
|
|
8444
|
-
critic: totalCriticTokens,
|
|
8445
|
-
},
|
|
8446
|
-
stalestPath,
|
|
8447
|
-
lastSynthesizedPath,
|
|
8448
|
-
lastSynthesizedAt,
|
|
8449
|
-
};
|
|
8450
|
-
}
|
|
8451
|
-
|
|
8452
|
-
/**
|
|
8453
|
-
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
8454
|
-
*
|
|
8455
|
-
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
8456
|
-
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
8457
|
-
*
|
|
8458
|
-
* @module discovery/discoverMetas
|
|
8459
|
-
*/
|
|
8460
|
-
/**
|
|
8461
|
-
* Discover all .meta/ directories via watcher walk.
|
|
8462
|
-
*
|
|
8463
|
-
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
8464
|
-
* and returns deduplicated meta directory paths.
|
|
8465
|
-
*
|
|
8466
|
-
* @param watcher - WatcherClient for walk queries.
|
|
8467
|
-
* @returns Array of normalized .meta/ directory paths.
|
|
8468
|
-
*/
|
|
8469
|
-
async function discoverMetas(watcher) {
|
|
8470
|
-
const allPaths = await watcher.walk(['**/.meta/meta.json']);
|
|
8471
|
-
// Deduplicate by .meta/ directory path (handles multi-chunk files)
|
|
8472
|
-
const seen = new Set();
|
|
8473
|
-
const metaPaths = [];
|
|
8474
|
-
for (const filePath of allPaths) {
|
|
8475
|
-
const fp = normalizePath(filePath);
|
|
8476
|
-
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
8477
|
-
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
8478
|
-
if (seen.has(metaPath))
|
|
8479
|
-
continue;
|
|
8480
|
-
seen.add(metaPath);
|
|
8481
|
-
metaPaths.push(metaPath);
|
|
8482
|
-
}
|
|
8483
|
-
return metaPaths;
|
|
8484
|
-
}
|
|
8485
|
-
|
|
8486
|
-
/**
|
|
8487
|
-
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
8488
|
-
*
|
|
8489
|
-
* Lock file: .meta/.lock containing `_lockPid` + `_lockStartedAt` (underscore-prefixed
|
|
8490
|
-
* reserved keys, consistent with meta.json conventions).
|
|
8491
|
-
* Stale timeout: 30 minutes.
|
|
8492
|
-
*
|
|
8493
|
-
* @module lock
|
|
8494
|
-
*/
|
|
8495
|
-
const LOCK_FILE = '.lock';
|
|
8496
|
-
/**
|
|
8497
|
-
* Resolve a path to a .meta directory.
|
|
8498
|
-
*
|
|
8499
|
-
* If the path already ends with '.meta', returns it as-is.
|
|
8500
|
-
* Otherwise, appends '.meta' as a subdirectory.
|
|
8501
|
-
*
|
|
8502
|
-
* @param inputPath - Path that may or may not end with '.meta'.
|
|
8503
|
-
* @returns The resolved .meta directory path.
|
|
8504
|
-
*/
|
|
8505
|
-
function resolveMetaDir(inputPath) {
|
|
8506
|
-
return inputPath.endsWith('.meta') ? inputPath : join(inputPath, '.meta');
|
|
8507
|
-
}
|
|
8508
|
-
const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
8509
|
-
/**
|
|
8510
|
-
* Read and classify the state of a .meta/.lock file.
|
|
8511
|
-
*
|
|
8512
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
8513
|
-
* @returns Parsed lock state.
|
|
8514
|
-
*/
|
|
8515
|
-
function readLockState(metaPath) {
|
|
8516
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
8517
|
-
if (!existsSync(lockPath)) {
|
|
8518
|
-
return { exists: false, staged: false, active: false, data: null };
|
|
8519
|
-
}
|
|
8520
|
-
try {
|
|
8521
|
-
const raw = readFileSync(lockPath, 'utf8');
|
|
8522
|
-
const data = JSON.parse(raw);
|
|
8523
|
-
if ('_id' in data) {
|
|
8524
|
-
return { exists: true, staged: true, active: false, data };
|
|
8525
|
-
}
|
|
8526
|
-
const startedAt = data._lockStartedAt;
|
|
8527
|
-
if (startedAt) {
|
|
8528
|
-
const lockAge = Date.now() - new Date(startedAt).getTime();
|
|
8529
|
-
return {
|
|
8530
|
-
exists: true,
|
|
8531
|
-
staged: false,
|
|
8532
|
-
active: lockAge < STALE_TIMEOUT_MS,
|
|
8533
|
-
data,
|
|
8534
|
-
};
|
|
8535
|
-
}
|
|
8536
|
-
return { exists: true, staged: false, active: false, data };
|
|
8537
|
-
}
|
|
8538
|
-
catch {
|
|
8539
|
-
return { exists: true, staged: false, active: false, data: null };
|
|
8540
|
-
}
|
|
8541
|
-
}
|
|
8542
|
-
/**
|
|
8543
|
-
* Attempt to acquire a lock on a .meta directory.
|
|
8544
|
-
*
|
|
8545
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
8546
|
-
* @returns True if lock was acquired, false if already locked (non-stale).
|
|
8547
|
-
*/
|
|
8548
|
-
function acquireLock(metaPath) {
|
|
8549
|
-
const state = readLockState(metaPath);
|
|
8550
|
-
// Active non-stale lock — cannot acquire
|
|
8551
|
-
if (state.active)
|
|
8552
|
-
return false;
|
|
8553
|
-
// Staged, stale, corrupt, or missing — safe to (over)write
|
|
8554
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
8555
|
-
const lock = {
|
|
8556
|
-
_lockPid: process.pid,
|
|
8557
|
-
_lockStartedAt: new Date().toISOString(),
|
|
8558
|
-
};
|
|
8559
|
-
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
8560
|
-
return true;
|
|
8561
|
-
}
|
|
8562
|
-
/**
|
|
8563
|
-
* Release a lock on a .meta directory.
|
|
8564
|
-
*
|
|
8565
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
8566
|
-
*/
|
|
8567
|
-
function releaseLock(metaPath) {
|
|
8568
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
8569
|
-
try {
|
|
8570
|
-
unlinkSync(lockPath);
|
|
8571
|
-
}
|
|
8572
|
-
catch {
|
|
8573
|
-
// Already removed or never existed
|
|
8574
|
-
}
|
|
8575
|
-
}
|
|
8576
|
-
/**
|
|
8577
|
-
* Check if a .meta directory is currently locked (non-stale).
|
|
8578
|
-
*
|
|
8579
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
8580
|
-
* @returns True if locked and not stale.
|
|
8581
|
-
*/
|
|
8582
|
-
function isLocked(metaPath) {
|
|
8583
|
-
return readLockState(metaPath).active;
|
|
8584
|
-
}
|
|
8585
|
-
/**
|
|
8586
|
-
* Clean up stale lock files on startup.
|
|
8587
|
-
*
|
|
8588
|
-
* For each .meta directory found via the provided paths:
|
|
8589
|
-
* - If lock contains PID-only data (synthesis incomplete), delete it.
|
|
8590
|
-
* - If lock contains staged result (_id present), log warning and delete.
|
|
8591
|
-
*
|
|
8592
|
-
* @param metaPaths - Array of .meta directory paths to check.
|
|
8593
|
-
* @param logger - Optional logger for warnings.
|
|
8594
|
-
*/
|
|
8595
|
-
function cleanupStaleLocks(metaPaths, logger) {
|
|
8596
|
-
for (const metaPath of metaPaths) {
|
|
8597
|
-
const state = readLockState(metaPath);
|
|
8598
|
-
if (!state.exists)
|
|
8599
|
-
continue;
|
|
8600
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
8601
|
-
if (state.staged) {
|
|
8602
|
-
logger?.warn({ metaPath }, 'Found staged synthesis result in lock file from previous crash — deleting (conservative: not auto-finalizing)');
|
|
8603
|
-
}
|
|
8604
|
-
else {
|
|
8605
|
-
logger?.warn({ metaPath }, 'Found stale lock file from previous crash — deleting');
|
|
8606
|
-
}
|
|
8607
|
-
try {
|
|
8608
|
-
unlinkSync(lockPath);
|
|
8609
|
-
}
|
|
8610
|
-
catch {
|
|
8611
|
-
// Already gone
|
|
8612
|
-
}
|
|
8613
|
-
}
|
|
8614
|
-
}
|
|
8615
|
-
|
|
8616
|
-
/**
|
|
8617
|
-
* Read and parse a meta.json file from a `.meta/` directory.
|
|
8618
|
-
*
|
|
8619
|
-
* Shared utility to eliminate repeated `JSON.parse(readFileSync(...))` across
|
|
8620
|
-
* discovery, orchestration, and route handlers.
|
|
8497
|
+
* Shared utility to eliminate repeated `JSON.parse(readFileSync(...))` across
|
|
8498
|
+
* discovery, orchestration, and route handlers.
|
|
8621
8499
|
*
|
|
8622
8500
|
* @module readMetaJson
|
|
8623
8501
|
*/
|
|
@@ -9030,22 +8908,287 @@ function getDeltaFiles(generatedAt, scopeFiles) {
|
|
|
9030
8908
|
}
|
|
9031
8909
|
|
|
9032
8910
|
/**
|
|
9033
|
-
*
|
|
8911
|
+
* In-memory cache for listMetas results with TTL and concurrent refresh guard.
|
|
9034
8912
|
*
|
|
9035
|
-
* @module
|
|
8913
|
+
* @module cache
|
|
9036
8914
|
*/
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
8915
|
+
const TTL_MS = 60_000;
|
|
8916
|
+
/**
|
|
8917
|
+
* Caches listMetas results to avoid expensive repeated filesystem walks.
|
|
8918
|
+
* Supports concurrent refresh coalescing and manual invalidation.
|
|
8919
|
+
*/
|
|
8920
|
+
class MetaCache {
|
|
8921
|
+
result = null;
|
|
8922
|
+
updatedAt = 0;
|
|
8923
|
+
refreshPromise = null;
|
|
8924
|
+
/** Get cached result or refresh if stale. */
|
|
8925
|
+
async get(config, watcher) {
|
|
8926
|
+
if (this.result && Date.now() - this.updatedAt < TTL_MS) {
|
|
8927
|
+
return this.result;
|
|
8928
|
+
}
|
|
8929
|
+
return this.refresh(config, watcher);
|
|
8930
|
+
}
|
|
8931
|
+
/** Force-expire the cache so next get() triggers a refresh. */
|
|
8932
|
+
invalidate() {
|
|
8933
|
+
this.updatedAt = 0;
|
|
8934
|
+
}
|
|
8935
|
+
async refresh(config, watcher) {
|
|
8936
|
+
if (this.refreshPromise)
|
|
8937
|
+
return this.refreshPromise;
|
|
8938
|
+
this.refreshPromise = listMetas(config, watcher)
|
|
8939
|
+
.then((result) => {
|
|
8940
|
+
this.result = result;
|
|
8941
|
+
this.updatedAt = Date.now();
|
|
8942
|
+
return result;
|
|
8943
|
+
})
|
|
8944
|
+
.finally(() => {
|
|
8945
|
+
this.refreshPromise = null;
|
|
8946
|
+
});
|
|
8947
|
+
return this.refreshPromise;
|
|
9042
8948
|
}
|
|
9043
8949
|
}
|
|
9044
8950
|
|
|
9045
8951
|
/**
|
|
9046
|
-
*
|
|
8952
|
+
* Shared live config hot-reload support.
|
|
9047
8953
|
*
|
|
9048
|
-
*
|
|
8954
|
+
* Used by both file-watch reloads in bootstrap and POST /config/apply
|
|
8955
|
+
* via the component descriptor's onConfigApply callback.
|
|
8956
|
+
*
|
|
8957
|
+
* @module configHotReload
|
|
8958
|
+
*/
|
|
8959
|
+
/**
|
|
8960
|
+
* Fields that require a service restart to take effect.
|
|
8961
|
+
*
|
|
8962
|
+
* Shared between the descriptor's `onConfigApply` and the file-watcher
|
|
8963
|
+
* hot-reload in `bootstrap.ts`.
|
|
8964
|
+
*/
|
|
8965
|
+
const RESTART_REQUIRED_FIELDS = [
|
|
8966
|
+
'port',
|
|
8967
|
+
'watcherUrl',
|
|
8968
|
+
'gatewayUrl',
|
|
8969
|
+
'gatewayApiKey',
|
|
8970
|
+
'defaultArchitect',
|
|
8971
|
+
'defaultCritic',
|
|
8972
|
+
];
|
|
8973
|
+
let runtime = null;
|
|
8974
|
+
/** Register the active service runtime for config-apply hot reload. */
|
|
8975
|
+
function registerConfigHotReloadRuntime(nextRuntime) {
|
|
8976
|
+
runtime = nextRuntime;
|
|
8977
|
+
}
|
|
8978
|
+
/** Apply hot-reloadable config changes to the live shared config object. */
|
|
8979
|
+
function applyHotReloadedConfig(newConfig) {
|
|
8980
|
+
if (!runtime)
|
|
8981
|
+
return;
|
|
8982
|
+
const { config, logger, scheduler } = runtime;
|
|
8983
|
+
for (const field of RESTART_REQUIRED_FIELDS) {
|
|
8984
|
+
const oldVal = config[field];
|
|
8985
|
+
const nextVal = newConfig[field];
|
|
8986
|
+
if (oldVal !== nextVal) {
|
|
8987
|
+
logger.warn({ field, oldValue: oldVal, newValue: nextVal }, 'Config field changed but requires restart to take effect');
|
|
8988
|
+
}
|
|
8989
|
+
}
|
|
8990
|
+
if (newConfig.schedule !== config.schedule) {
|
|
8991
|
+
scheduler?.updateSchedule(newConfig.schedule);
|
|
8992
|
+
config.schedule = newConfig.schedule;
|
|
8993
|
+
logger.info({ schedule: newConfig.schedule }, 'Schedule hot-reloaded');
|
|
8994
|
+
}
|
|
8995
|
+
if (newConfig.logging.level !== config.logging.level) {
|
|
8996
|
+
logger.level = newConfig.logging.level;
|
|
8997
|
+
config.logging.level = newConfig.logging.level;
|
|
8998
|
+
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
8999
|
+
}
|
|
9000
|
+
const restartSet = new Set(RESTART_REQUIRED_FIELDS);
|
|
9001
|
+
for (const key of Object.keys(newConfig)) {
|
|
9002
|
+
if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
|
|
9003
|
+
continue;
|
|
9004
|
+
}
|
|
9005
|
+
const oldVal = config[key];
|
|
9006
|
+
const nextVal = newConfig[key];
|
|
9007
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(nextVal)) {
|
|
9008
|
+
config[key] = nextVal;
|
|
9009
|
+
logger.info({ field: key }, 'Config field hot-reloaded');
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
9012
|
+
}
|
|
9013
|
+
|
|
9014
|
+
/**
|
|
9015
|
+
* Zod schema for jeeves-meta service configuration.
|
|
9016
|
+
*
|
|
9017
|
+
* The service config is a strict superset of the core (library-compatible) meta config.
|
|
9018
|
+
*
|
|
9019
|
+
* @module schema/config
|
|
9020
|
+
*/
|
|
9021
|
+
/** Zod schema for logging configuration. */
|
|
9022
|
+
const loggingSchema = z.object({
|
|
9023
|
+
/** Log level. */
|
|
9024
|
+
level: z.string().default('info'),
|
|
9025
|
+
/** Optional file path for log output. */
|
|
9026
|
+
file: z.string().optional(),
|
|
9027
|
+
});
|
|
9028
|
+
/** Zod schema for a single auto-seed policy rule. */
|
|
9029
|
+
const autoSeedRuleSchema = z.object({
|
|
9030
|
+
/** Glob pattern matched against watcher walk results. */
|
|
9031
|
+
match: z.string(),
|
|
9032
|
+
/** Optional steering prompt for seeded metas. */
|
|
9033
|
+
steer: z.string().optional(),
|
|
9034
|
+
/** Optional cross-references for seeded metas. */
|
|
9035
|
+
crossRefs: z.array(z.string()).optional(),
|
|
9036
|
+
});
|
|
9037
|
+
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
9038
|
+
const serviceConfigSchema = metaConfigSchema.extend({
|
|
9039
|
+
/** HTTP port for the service (default: 1938). */
|
|
9040
|
+
port: z.number().int().min(1).max(65535).default(1938),
|
|
9041
|
+
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
9042
|
+
schedule: z.string().default('*/30 * * * *'),
|
|
9043
|
+
/** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
|
|
9044
|
+
reportChannel: z.string().optional(),
|
|
9045
|
+
/** Channel/user ID to send progress messages to. */
|
|
9046
|
+
reportTarget: z.string().optional(),
|
|
9047
|
+
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
9048
|
+
serverBaseUrl: z.string().optional(),
|
|
9049
|
+
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
9050
|
+
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
9051
|
+
/** Logging configuration. */
|
|
9052
|
+
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
9053
|
+
/**
|
|
9054
|
+
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
9055
|
+
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
9056
|
+
*/
|
|
9057
|
+
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
9058
|
+
});
|
|
9059
|
+
|
|
9060
|
+
/**
|
|
9061
|
+
* Load and resolve jeeves-meta service config.
|
|
9062
|
+
*
|
|
9063
|
+
* Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
|
|
9064
|
+
*
|
|
9065
|
+
* @module configLoader
|
|
9066
|
+
*/
|
|
9067
|
+
/**
|
|
9068
|
+
* Deep-walk a value, replacing `\${VAR\}` patterns with process.env values.
|
|
9069
|
+
*
|
|
9070
|
+
* @param value - Arbitrary JSON-compatible value.
|
|
9071
|
+
* @returns Value with env-var placeholders resolved.
|
|
9072
|
+
*/
|
|
9073
|
+
function substituteEnvVars(value) {
|
|
9074
|
+
if (typeof value === 'string') {
|
|
9075
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
|
|
9076
|
+
const envVal = process.env[name];
|
|
9077
|
+
if (envVal === undefined) {
|
|
9078
|
+
throw new Error(`Environment variable ${name} is not set`);
|
|
9079
|
+
}
|
|
9080
|
+
return envVal;
|
|
9081
|
+
});
|
|
9082
|
+
}
|
|
9083
|
+
if (Array.isArray(value)) {
|
|
9084
|
+
return value.map(substituteEnvVars);
|
|
9085
|
+
}
|
|
9086
|
+
if (value !== null && typeof value === 'object') {
|
|
9087
|
+
const result = {};
|
|
9088
|
+
for (const [key, val] of Object.entries(value)) {
|
|
9089
|
+
result[key] = substituteEnvVars(val);
|
|
9090
|
+
}
|
|
9091
|
+
return result;
|
|
9092
|
+
}
|
|
9093
|
+
return value;
|
|
9094
|
+
}
|
|
9095
|
+
/**
|
|
9096
|
+
* Resolve \@file: references in a config value.
|
|
9097
|
+
*
|
|
9098
|
+
* @param value - String value that may start with "\@file:".
|
|
9099
|
+
* @param baseDir - Base directory for resolving relative paths.
|
|
9100
|
+
* @returns The resolved string (file contents or original value).
|
|
9101
|
+
*/
|
|
9102
|
+
function resolveFileRef(value, baseDir) {
|
|
9103
|
+
if (!value.startsWith('@file:'))
|
|
9104
|
+
return value;
|
|
9105
|
+
const filePath = join(baseDir, value.slice(6));
|
|
9106
|
+
return readFileSync(filePath, 'utf8');
|
|
9107
|
+
}
|
|
9108
|
+
/**
|
|
9109
|
+
* Migrate legacy config path to the new canonical location.
|
|
9110
|
+
*
|
|
9111
|
+
* If the old path `{configRoot}/jeeves-meta.config.json` exists and the new
|
|
9112
|
+
* path `{configRoot}/jeeves-meta/config.json` does NOT exist, copies the file
|
|
9113
|
+
* to the new location and logs a warning.
|
|
9114
|
+
*
|
|
9115
|
+
* @param configRoot - Root directory for configuration files.
|
|
9116
|
+
* @param warn - Optional callback for logging the migration warning.
|
|
9117
|
+
*/
|
|
9118
|
+
function migrateConfigPath(configRoot, warn) {
|
|
9119
|
+
const oldPath = join(configRoot, 'jeeves-meta.config.json');
|
|
9120
|
+
const newDir = join(configRoot, 'jeeves-meta');
|
|
9121
|
+
const newPath = join(newDir, 'config.json');
|
|
9122
|
+
if (existsSync(oldPath) && !existsSync(newPath)) {
|
|
9123
|
+
mkdirSync(newDir, { recursive: true });
|
|
9124
|
+
copyFileSync(oldPath, newPath);
|
|
9125
|
+
const message = `Migrated config from ${oldPath} to ${newPath}. The old file can be removed.`;
|
|
9126
|
+
if (warn) {
|
|
9127
|
+
warn(message);
|
|
9128
|
+
}
|
|
9129
|
+
else {
|
|
9130
|
+
console.warn(`[jeeves-meta] ${message}`);
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
9133
|
+
}
|
|
9134
|
+
/**
|
|
9135
|
+
* Resolve config path from --config flag or JEEVES_META_CONFIG env var.
|
|
9136
|
+
*
|
|
9137
|
+
* @param args - CLI arguments (process.argv.slice(2)).
|
|
9138
|
+
* @returns Resolved config path.
|
|
9139
|
+
* @throws If no config path found.
|
|
9140
|
+
*/
|
|
9141
|
+
function resolveConfigPath(args) {
|
|
9142
|
+
let configIdx = args.indexOf('--config');
|
|
9143
|
+
if (configIdx === -1)
|
|
9144
|
+
configIdx = args.indexOf('-c');
|
|
9145
|
+
if (configIdx !== -1 && args[configIdx + 1]) {
|
|
9146
|
+
return args[configIdx + 1];
|
|
9147
|
+
}
|
|
9148
|
+
const envPath = process.env['JEEVES_META_CONFIG'];
|
|
9149
|
+
if (envPath)
|
|
9150
|
+
return envPath;
|
|
9151
|
+
throw new Error('Config path required. Use --config <path> or set JEEVES_META_CONFIG env var.');
|
|
9152
|
+
}
|
|
9153
|
+
/**
|
|
9154
|
+
* Load service config from a JSON file.
|
|
9155
|
+
*
|
|
9156
|
+
* Resolves \@file: references for defaultArchitect and defaultCritic,
|
|
9157
|
+
* and substitutes environment-variable placeholders throughout.
|
|
9158
|
+
*
|
|
9159
|
+
* @param configPath - Path to config JSON file.
|
|
9160
|
+
* @returns Validated ServiceConfig.
|
|
9161
|
+
*/
|
|
9162
|
+
function loadServiceConfig(configPath) {
|
|
9163
|
+
const rawText = readFileSync(configPath, 'utf8');
|
|
9164
|
+
const raw = substituteEnvVars(JSON.parse(rawText));
|
|
9165
|
+
const baseDir = dirname(configPath);
|
|
9166
|
+
if (typeof raw['defaultArchitect'] === 'string') {
|
|
9167
|
+
raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
|
|
9168
|
+
}
|
|
9169
|
+
if (typeof raw['defaultCritic'] === 'string') {
|
|
9170
|
+
raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
|
|
9171
|
+
}
|
|
9172
|
+
return serviceConfigSchema.parse(raw);
|
|
9173
|
+
}
|
|
9174
|
+
|
|
9175
|
+
/**
|
|
9176
|
+
* Error thrown when a spawned subprocess is aborted via AbortController.
|
|
9177
|
+
*
|
|
9178
|
+
* @module executor/SpawnAbortedError
|
|
9179
|
+
*/
|
|
9180
|
+
/** Error indicating a spawn was deliberately aborted. */
|
|
9181
|
+
class SpawnAbortedError extends Error {
|
|
9182
|
+
constructor(message = 'Synthesis was aborted') {
|
|
9183
|
+
super(message);
|
|
9184
|
+
this.name = 'SpawnAbortedError';
|
|
9185
|
+
}
|
|
9186
|
+
}
|
|
9187
|
+
|
|
9188
|
+
/**
|
|
9189
|
+
* Error thrown when a spawned subprocess times out.
|
|
9190
|
+
*
|
|
9191
|
+
* Carries the output file path so callers can attempt partial output recovery.
|
|
9049
9192
|
*
|
|
9050
9193
|
* @module executor/SpawnTimeoutError
|
|
9051
9194
|
*/
|
|
@@ -9127,21 +9270,29 @@ class GatewayExecutor {
|
|
|
9127
9270
|
}
|
|
9128
9271
|
return data;
|
|
9129
9272
|
}
|
|
9130
|
-
/** Look up
|
|
9131
|
-
async
|
|
9273
|
+
/** Look up session metadata (tokens, completion status) via sessions_list. */
|
|
9274
|
+
async getSessionInfo(sessionKey) {
|
|
9132
9275
|
try {
|
|
9133
9276
|
const result = await this.invoke('sessions_list', {
|
|
9134
|
-
limit:
|
|
9277
|
+
limit: 200,
|
|
9135
9278
|
messageLimit: 0,
|
|
9136
9279
|
});
|
|
9137
9280
|
const sessions = (result.result?.details?.sessions ??
|
|
9138
9281
|
result.result?.sessions ??
|
|
9139
9282
|
[]);
|
|
9140
9283
|
const match = sessions.find((s) => s.key === sessionKey);
|
|
9141
|
-
|
|
9284
|
+
if (!match) {
|
|
9285
|
+
// Session absent from list — likely cleaned up after completion.
|
|
9286
|
+
// With limit=200 this is reliable; a false positive here only
|
|
9287
|
+
// means we read the output file slightly early (still correct
|
|
9288
|
+
// if the file exists).
|
|
9289
|
+
return { completed: true };
|
|
9290
|
+
}
|
|
9291
|
+
const done = match.status === 'completed' || match.status === 'done';
|
|
9292
|
+
return { tokens: match.totalTokens, completed: done };
|
|
9142
9293
|
}
|
|
9143
9294
|
catch {
|
|
9144
|
-
return
|
|
9295
|
+
return { completed: false };
|
|
9145
9296
|
}
|
|
9146
9297
|
}
|
|
9147
9298
|
/** Whether this executor has been aborted by the operator. */
|
|
@@ -9183,8 +9334,10 @@ class GatewayExecutor {
|
|
|
9183
9334
|
...(options?.thinking ? { thinking: options.thinking } : {}),
|
|
9184
9335
|
...(options?.model ? { model: options.model } : {}),
|
|
9185
9336
|
});
|
|
9186
|
-
const details = (spawnResult.result?.details ??
|
|
9187
|
-
|
|
9337
|
+
const details = (spawnResult.result?.details ??
|
|
9338
|
+
spawnResult.result ??
|
|
9339
|
+
{});
|
|
9340
|
+
const sessionKey = details.childSessionKey ?? details.sessionKey;
|
|
9188
9341
|
if (typeof sessionKey !== 'string' || !sessionKey) {
|
|
9189
9342
|
throw new Error('Gateway sessions_spawn returned no sessionKey: ' +
|
|
9190
9343
|
JSON.stringify(spawnResult));
|
|
@@ -9207,48 +9360,53 @@ class GatewayExecutor {
|
|
|
9207
9360
|
historyResult.result?.messages ??
|
|
9208
9361
|
[];
|
|
9209
9362
|
const msgArray = messages;
|
|
9363
|
+
// Check 1: terminal stop reason in history
|
|
9364
|
+
let historyDone = false;
|
|
9210
9365
|
if (msgArray.length > 0) {
|
|
9211
9366
|
const lastMsg = msgArray[msgArray.length - 1];
|
|
9212
|
-
// Complete when last message is assistant with a terminal stop reason
|
|
9213
9367
|
if (lastMsg.role === 'assistant' &&
|
|
9214
9368
|
lastMsg.stopReason &&
|
|
9215
9369
|
lastMsg.stopReason !== 'toolUse' &&
|
|
9216
9370
|
lastMsg.stopReason !== 'error') {
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9371
|
+
historyDone = true;
|
|
9372
|
+
}
|
|
9373
|
+
}
|
|
9374
|
+
// Check 2: session completion status via sessions_list
|
|
9375
|
+
const sessionInfo = await this.getSessionInfo(sessionKey);
|
|
9376
|
+
if (historyDone || sessionInfo.completed) {
|
|
9377
|
+
const tokens = sessionInfo.tokens;
|
|
9378
|
+
// Read output from file (sub-agent wrote it via Write tool)
|
|
9379
|
+
if (existsSync(outputPath)) {
|
|
9380
|
+
try {
|
|
9381
|
+
const output = readFileSync(outputPath, 'utf8');
|
|
9382
|
+
return { output, tokens };
|
|
9383
|
+
}
|
|
9384
|
+
finally {
|
|
9221
9385
|
try {
|
|
9222
|
-
|
|
9223
|
-
return { output, tokens };
|
|
9386
|
+
unlinkSync(outputPath);
|
|
9224
9387
|
}
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
unlinkSync(outputPath);
|
|
9228
|
-
}
|
|
9229
|
-
catch {
|
|
9230
|
-
/* cleanup best-effort */
|
|
9231
|
-
}
|
|
9388
|
+
catch {
|
|
9389
|
+
/* cleanup best-effort */
|
|
9232
9390
|
}
|
|
9233
9391
|
}
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9392
|
+
}
|
|
9393
|
+
// Fallback: extract from message content if file wasn't written
|
|
9394
|
+
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9395
|
+
const msg = msgArray[i];
|
|
9396
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
9397
|
+
const text = typeof msg.content === 'string'
|
|
9398
|
+
? msg.content
|
|
9399
|
+
: Array.isArray(msg.content)
|
|
9239
9400
|
? msg.content
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
if (text)
|
|
9247
|
-
return { output: text, tokens };
|
|
9248
|
-
}
|
|
9401
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
9402
|
+
.map((b) => b.text)
|
|
9403
|
+
.join('\n')
|
|
9404
|
+
: '';
|
|
9405
|
+
if (text)
|
|
9406
|
+
return { output: text, tokens };
|
|
9249
9407
|
}
|
|
9250
|
-
return { output: '', tokens };
|
|
9251
9408
|
}
|
|
9409
|
+
return { output: '', tokens };
|
|
9252
9410
|
}
|
|
9253
9411
|
}
|
|
9254
9412
|
catch {
|
|
@@ -9423,6 +9581,7 @@ async function buildContextPackage(node, meta, watcher, logger) {
|
|
|
9423
9581
|
*
|
|
9424
9582
|
* @module orchestrator/buildTask
|
|
9425
9583
|
*/
|
|
9584
|
+
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
9426
9585
|
/** Build the template context from synthesis inputs. */
|
|
9427
9586
|
function buildTemplateContext(ctx, meta, config) {
|
|
9428
9587
|
return {
|
|
@@ -9578,264 +9737,16 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
9578
9737
|
}
|
|
9579
9738
|
|
|
9580
9739
|
/**
|
|
9581
|
-
*
|
|
9740
|
+
* Build a minimal MetaNode from a known meta path using watcher walk.
|
|
9582
9741
|
*
|
|
9583
|
-
*
|
|
9742
|
+
* Used for targeted synthesis (when a specific path is requested) to avoid
|
|
9743
|
+
* the full discovery + ownership tree build. Discovers only immediate child
|
|
9744
|
+
* `.meta/` directories.
|
|
9745
|
+
*
|
|
9746
|
+
* @module discovery/buildMinimalNode
|
|
9584
9747
|
*/
|
|
9585
|
-
const DEFAULT_DECAY = 0.3;
|
|
9586
9748
|
/**
|
|
9587
|
-
*
|
|
9588
|
-
*
|
|
9589
|
-
* @param current - New observation.
|
|
9590
|
-
* @param previous - Previous EMA value, or undefined for first observation.
|
|
9591
|
-
* @param decay - Decay factor (0-1). Higher = more weight on new value. Default 0.3.
|
|
9592
|
-
* @returns Updated EMA.
|
|
9593
|
-
*/
|
|
9594
|
-
function computeEma(current, previous, decay = DEFAULT_DECAY) {
|
|
9595
|
-
if (previous === undefined)
|
|
9596
|
-
return current;
|
|
9597
|
-
return decay * current + (1 - decay) * previous;
|
|
9598
|
-
}
|
|
9599
|
-
|
|
9600
|
-
/**
|
|
9601
|
-
* Structured error from a synthesis step failure.
|
|
9602
|
-
*
|
|
9603
|
-
* @module schema/error
|
|
9604
|
-
*/
|
|
9605
|
-
/** Zod schema for synthesis step errors. */
|
|
9606
|
-
const metaErrorSchema = z.object({
|
|
9607
|
-
/** Which step failed: 'architect', 'builder', or 'critic'. */
|
|
9608
|
-
step: z.enum(['architect', 'builder', 'critic']),
|
|
9609
|
-
/** Error classification code. */
|
|
9610
|
-
code: z.string(),
|
|
9611
|
-
/** Human-readable error message. */
|
|
9612
|
-
message: z.string(),
|
|
9613
|
-
});
|
|
9614
|
-
|
|
9615
|
-
/**
|
|
9616
|
-
* Zod schema for .meta/meta.json files.
|
|
9617
|
-
*
|
|
9618
|
-
* Reserved properties are underscore-prefixed and engine-managed.
|
|
9619
|
-
* All other keys are open schema (builder output).
|
|
9620
|
-
*
|
|
9621
|
-
* @module schema/meta
|
|
9622
|
-
*/
|
|
9623
|
-
/** Valid states for a synthesis phase. */
|
|
9624
|
-
const phaseStatuses = [
|
|
9625
|
-
'fresh',
|
|
9626
|
-
'stale',
|
|
9627
|
-
'pending',
|
|
9628
|
-
'running',
|
|
9629
|
-
'failed',
|
|
9630
|
-
];
|
|
9631
|
-
/** Zod schema for a per-phase status value. */
|
|
9632
|
-
const phaseStatusSchema = z.enum(phaseStatuses);
|
|
9633
|
-
/** Zod schema for the per-meta phase state record. */
|
|
9634
|
-
const phaseStateSchema = z.object({
|
|
9635
|
-
architect: phaseStatusSchema,
|
|
9636
|
-
builder: phaseStatusSchema,
|
|
9637
|
-
critic: phaseStatusSchema,
|
|
9638
|
-
});
|
|
9639
|
-
/** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
|
|
9640
|
-
const metaJsonSchema = z
|
|
9641
|
-
.object({
|
|
9642
|
-
/** Stable identity. Auto-generated on first synthesis if not provided. */
|
|
9643
|
-
_id: z.uuid().optional(),
|
|
9644
|
-
/** Human-provided steering prompt. Optional. */
|
|
9645
|
-
_steer: z.string().optional(),
|
|
9646
|
-
/**
|
|
9647
|
-
* Explicit cross-references to other meta owner paths.
|
|
9648
|
-
* Referenced metas' _content is included as architect/builder context.
|
|
9649
|
-
*/
|
|
9650
|
-
_crossRefs: z.array(z.string()).optional(),
|
|
9651
|
-
/** Architect system prompt used this turn. Defaults from config. */
|
|
9652
|
-
_architect: z.string().optional(),
|
|
9653
|
-
/**
|
|
9654
|
-
* Task brief generated by the architect. Cached and reused across cycles;
|
|
9655
|
-
* regenerated only when triggered.
|
|
9656
|
-
*/
|
|
9657
|
-
_builder: z.string().optional(),
|
|
9658
|
-
/** Critic system prompt used this turn. Defaults from config. */
|
|
9659
|
-
_critic: z.string().optional(),
|
|
9660
|
-
/** Timestamp of last synthesis. ISO 8601. */
|
|
9661
|
-
_generatedAt: z.iso.datetime().optional(),
|
|
9662
|
-
/** Narrative synthesis output. Rendered by watcher for embedding. */
|
|
9663
|
-
_content: z.string().optional(),
|
|
9664
|
-
/**
|
|
9665
|
-
* Hash of sorted file listing in scope. Detects directory structure
|
|
9666
|
-
* changes that trigger an architect re-run.
|
|
9667
|
-
*/
|
|
9668
|
-
_structureHash: z.string().optional(),
|
|
9669
|
-
/**
|
|
9670
|
-
* Cycles since last architect run. Reset to 0 when architect runs.
|
|
9671
|
-
* Used with architectEvery to trigger periodic re-prompting.
|
|
9672
|
-
*/
|
|
9673
|
-
_synthesisCount: z.number().int().min(0).optional(),
|
|
9674
|
-
/** Critic evaluation of the last synthesis. */
|
|
9675
|
-
_feedback: z.string().optional(),
|
|
9676
|
-
/**
|
|
9677
|
-
* Present and true on archive snapshots. Distinguishes live vs. archived
|
|
9678
|
-
* metas.
|
|
9679
|
-
*/
|
|
9680
|
-
_archived: z.boolean().optional(),
|
|
9681
|
-
/** Timestamp when this snapshot was archived. ISO 8601. */
|
|
9682
|
-
_archivedAt: z.iso.datetime().optional(),
|
|
9683
|
-
/**
|
|
9684
|
-
* Scheduling priority. Higher = updates more often. Negative allowed;
|
|
9685
|
-
* normalized to min 0 at scheduling time.
|
|
9686
|
-
*/
|
|
9687
|
-
_depth: z.number().optional(),
|
|
9688
|
-
/**
|
|
9689
|
-
* Emphasis multiplier for depth weighting in scheduling.
|
|
9690
|
-
* Default 1. Higher values increase this meta's scheduling priority
|
|
9691
|
-
* relative to its depth. Set to 0.5 to halve the depth effect,
|
|
9692
|
-
* 2 to double it, 0 to ignore depth entirely for this meta.
|
|
9693
|
-
*/
|
|
9694
|
-
_emphasis: z.number().min(0).optional(),
|
|
9695
|
-
/** Token count from last architect subprocess call. */
|
|
9696
|
-
_architectTokens: z.number().int().optional(),
|
|
9697
|
-
/** Token count from last builder subprocess call. */
|
|
9698
|
-
_builderTokens: z.number().int().optional(),
|
|
9699
|
-
/** Token count from last critic subprocess call. */
|
|
9700
|
-
_criticTokens: z.number().int().optional(),
|
|
9701
|
-
/** Exponential moving average of architect token usage (decay 0.3). */
|
|
9702
|
-
_architectTokensAvg: z.number().optional(),
|
|
9703
|
-
/** Exponential moving average of builder token usage (decay 0.3). */
|
|
9704
|
-
_builderTokensAvg: z.number().optional(),
|
|
9705
|
-
/** Exponential moving average of critic token usage (decay 0.3). */
|
|
9706
|
-
_criticTokensAvg: z.number().optional(),
|
|
9707
|
-
/**
|
|
9708
|
-
* Opaque state carried across synthesis cycles for progressive work.
|
|
9709
|
-
* Set by the builder, passed back as context on next cycle.
|
|
9710
|
-
*/
|
|
9711
|
-
_state: z.unknown().optional(),
|
|
9712
|
-
/**
|
|
9713
|
-
* Structured error from last cycle. Present when a step failed.
|
|
9714
|
-
* Cleared on successful cycle.
|
|
9715
|
-
*/
|
|
9716
|
-
_error: metaErrorSchema.optional(),
|
|
9717
|
-
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
9718
|
-
_disabled: z.boolean().optional(),
|
|
9719
|
-
/**
|
|
9720
|
-
* Per-phase state machine record. Engine-managed.
|
|
9721
|
-
* Keyed by phase name (architect, builder, critic) with status values.
|
|
9722
|
-
* Persisted to survive ticks; derived on first load for back-compat.
|
|
9723
|
-
*/
|
|
9724
|
-
_phaseState: phaseStateSchema.optional(),
|
|
9725
|
-
})
|
|
9726
|
-
.loose();
|
|
9727
|
-
|
|
9728
|
-
/**
|
|
9729
|
-
* Merge synthesis results into meta.json.
|
|
9730
|
-
*
|
|
9731
|
-
* Preserves human-set fields (_id, _steer, _depth).
|
|
9732
|
-
* Writes engine fields (_generatedAt, _structureHash, etc.).
|
|
9733
|
-
* Validates against schema before writing.
|
|
9734
|
-
*
|
|
9735
|
-
* @module orchestrator/merge
|
|
9736
|
-
*/
|
|
9737
|
-
/**
|
|
9738
|
-
* Merge results into meta.json and write atomically.
|
|
9739
|
-
*
|
|
9740
|
-
* @param options - Merge options.
|
|
9741
|
-
* @returns The updated MetaJson.
|
|
9742
|
-
* @throws If validation fails (malformed output).
|
|
9743
|
-
*/
|
|
9744
|
-
async function mergeAndWrite(options) {
|
|
9745
|
-
const merged = {
|
|
9746
|
-
// Preserve human-set fields (auto-generate _id on first synthesis)
|
|
9747
|
-
_id: options.current._id ?? randomUUID(),
|
|
9748
|
-
_steer: options.current._steer,
|
|
9749
|
-
_depth: options.current._depth,
|
|
9750
|
-
_emphasis: options.current._emphasis,
|
|
9751
|
-
// Engine fields
|
|
9752
|
-
_architect: options.architect,
|
|
9753
|
-
_builder: options.builder,
|
|
9754
|
-
_critic: options.critic,
|
|
9755
|
-
_generatedAt: options.stateOnly
|
|
9756
|
-
? options.current._generatedAt
|
|
9757
|
-
: new Date().toISOString(),
|
|
9758
|
-
_structureHash: options.structureHash,
|
|
9759
|
-
_synthesisCount: options.synthesisCount,
|
|
9760
|
-
// Token tracking
|
|
9761
|
-
_architectTokens: options.architectTokens,
|
|
9762
|
-
_builderTokens: options.builderTokens,
|
|
9763
|
-
_criticTokens: options.criticTokens,
|
|
9764
|
-
_architectTokensAvg: options.architectTokens !== undefined
|
|
9765
|
-
? computeEma(options.architectTokens, options.current._architectTokensAvg)
|
|
9766
|
-
: options.current._architectTokensAvg,
|
|
9767
|
-
_builderTokensAvg: options.builderTokens !== undefined
|
|
9768
|
-
? computeEma(options.builderTokens, options.current._builderTokensAvg)
|
|
9769
|
-
: options.current._builderTokensAvg,
|
|
9770
|
-
_criticTokensAvg: options.criticTokens !== undefined
|
|
9771
|
-
? computeEma(options.criticTokens, options.current._criticTokensAvg)
|
|
9772
|
-
: options.current._criticTokensAvg,
|
|
9773
|
-
// Content from builder (stateOnly preserves previous content)
|
|
9774
|
-
_content: options.stateOnly
|
|
9775
|
-
? options.current._content
|
|
9776
|
-
: (options.builderOutput?.content ?? options.current._content),
|
|
9777
|
-
// Feedback from critic
|
|
9778
|
-
_feedback: options.feedback ?? options.current._feedback,
|
|
9779
|
-
// Progressive state
|
|
9780
|
-
_state: options.state,
|
|
9781
|
-
// Error handling
|
|
9782
|
-
_error: options.error ?? undefined,
|
|
9783
|
-
// Phase state machine
|
|
9784
|
-
_phaseState: options.phaseState,
|
|
9785
|
-
// Spread structured fields from builder
|
|
9786
|
-
...options.builderOutput?.fields,
|
|
9787
|
-
};
|
|
9788
|
-
// Clean up undefined optional fields
|
|
9789
|
-
if (merged._steer === undefined)
|
|
9790
|
-
delete merged._steer;
|
|
9791
|
-
if (merged._depth === undefined)
|
|
9792
|
-
delete merged._depth;
|
|
9793
|
-
if (merged._emphasis === undefined)
|
|
9794
|
-
delete merged._emphasis;
|
|
9795
|
-
if (merged._architectTokens === undefined)
|
|
9796
|
-
delete merged._architectTokens;
|
|
9797
|
-
if (merged._builderTokens === undefined)
|
|
9798
|
-
delete merged._builderTokens;
|
|
9799
|
-
if (merged._criticTokens === undefined)
|
|
9800
|
-
delete merged._criticTokens;
|
|
9801
|
-
if (merged._architectTokensAvg === undefined)
|
|
9802
|
-
delete merged._architectTokensAvg;
|
|
9803
|
-
if (merged._builderTokensAvg === undefined)
|
|
9804
|
-
delete merged._builderTokensAvg;
|
|
9805
|
-
if (merged._criticTokensAvg === undefined)
|
|
9806
|
-
delete merged._criticTokensAvg;
|
|
9807
|
-
if (merged._state === undefined)
|
|
9808
|
-
delete merged._state;
|
|
9809
|
-
if (merged._error === undefined)
|
|
9810
|
-
delete merged._error;
|
|
9811
|
-
if (merged._content === undefined)
|
|
9812
|
-
delete merged._content;
|
|
9813
|
-
if (merged._feedback === undefined)
|
|
9814
|
-
delete merged._feedback;
|
|
9815
|
-
if (merged._phaseState === undefined)
|
|
9816
|
-
delete merged._phaseState;
|
|
9817
|
-
// Validate
|
|
9818
|
-
const result = metaJsonSchema.safeParse(merged);
|
|
9819
|
-
if (!result.success) {
|
|
9820
|
-
throw new Error(`Meta validation failed: ${result.error.message}`);
|
|
9821
|
-
}
|
|
9822
|
-
// Write to specified path (lock staging) or default meta.json
|
|
9823
|
-
const filePath = options.outputPath ?? join(options.metaPath, 'meta.json');
|
|
9824
|
-
await writeFile(filePath, JSON.stringify(result.data, null, 2) + '\n');
|
|
9825
|
-
return result.data;
|
|
9826
|
-
}
|
|
9827
|
-
|
|
9828
|
-
/**
|
|
9829
|
-
* Build a minimal MetaNode from a known meta path using watcher walk.
|
|
9830
|
-
*
|
|
9831
|
-
* Used for targeted synthesis (when a specific path is requested) to avoid
|
|
9832
|
-
* the full discovery + ownership tree build. Discovers only immediate child
|
|
9833
|
-
* `.meta/` directories.
|
|
9834
|
-
*
|
|
9835
|
-
* @module discovery/buildMinimalNode
|
|
9836
|
-
*/
|
|
9837
|
-
/**
|
|
9838
|
-
* Build a minimal MetaNode for a known meta path.
|
|
9749
|
+
* Build a minimal MetaNode for a known meta path.
|
|
9839
9750
|
*
|
|
9840
9751
|
* Walks the owner directory for child `.meta/meta.json` files and constructs
|
|
9841
9752
|
* a shallow ownership tree (self + direct children only).
|
|
@@ -9887,103 +9798,301 @@ async function buildMinimalNode(metaPath, watcher) {
|
|
|
9887
9798
|
}
|
|
9888
9799
|
|
|
9889
9800
|
/**
|
|
9890
|
-
*
|
|
9801
|
+
* Pure phase-state transition functions.
|
|
9891
9802
|
*
|
|
9892
|
-
*
|
|
9803
|
+
* Implements every row of the §8 "Transitions and invalidation cascade" table.
|
|
9804
|
+
* No I/O — pure functions over PhaseState and documented inputs.
|
|
9893
9805
|
*
|
|
9894
|
-
* @module
|
|
9806
|
+
* @module phaseState/phaseTransitions
|
|
9895
9807
|
*/
|
|
9896
9808
|
/**
|
|
9897
|
-
*
|
|
9898
|
-
*
|
|
9899
|
-
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
9900
|
-
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
9901
|
-
*
|
|
9902
|
-
* Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
|
|
9903
|
-
* metas to tune how much their tree position affects scheduling.
|
|
9904
|
-
*
|
|
9905
|
-
* @param candidates - Array of \{ node, meta, actualStaleness \}.
|
|
9906
|
-
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
9907
|
-
* @returns Same array with effectiveStaleness computed.
|
|
9809
|
+
* Create a fresh (fully-complete) phase state.
|
|
9908
9810
|
*/
|
|
9909
|
-
function
|
|
9910
|
-
|
|
9911
|
-
return [];
|
|
9912
|
-
// Get depth for each candidate: use _depth override or tree depth
|
|
9913
|
-
const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
|
|
9914
|
-
// Normalize: shift so minimum becomes 0
|
|
9915
|
-
const minDepth = Math.min(...depths);
|
|
9916
|
-
const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
|
|
9917
|
-
return candidates.map((c, i) => {
|
|
9918
|
-
const emphasis = c.meta._emphasis ?? 1;
|
|
9919
|
-
return {
|
|
9920
|
-
...c,
|
|
9921
|
-
effectiveStaleness: c.actualStaleness *
|
|
9922
|
-
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
9923
|
-
};
|
|
9924
|
-
});
|
|
9811
|
+
function freshPhaseState() {
|
|
9812
|
+
return { architect: 'fresh', builder: 'fresh', critic: 'fresh' };
|
|
9925
9813
|
}
|
|
9926
|
-
|
|
9927
9814
|
/**
|
|
9928
|
-
*
|
|
9929
|
-
*
|
|
9930
|
-
* Picks the meta with highest effective staleness.
|
|
9931
|
-
*
|
|
9932
|
-
* @module scheduling/selectCandidate
|
|
9815
|
+
* Create a phase state for a never-synthesized meta (all pending from architect).
|
|
9933
9816
|
*/
|
|
9817
|
+
function initialPhaseState() {
|
|
9818
|
+
return { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
9819
|
+
}
|
|
9934
9820
|
/**
|
|
9935
|
-
*
|
|
9821
|
+
* Enforce the per-meta invariant: at most one phase is pending or running,
|
|
9822
|
+
* and it is the first non-fresh phase in pipeline order.
|
|
9936
9823
|
*
|
|
9937
|
-
*
|
|
9938
|
-
* @returns The winning candidate, or null if no candidates.
|
|
9824
|
+
* Stale phases that become the first non-fresh phase are promoted to pending.
|
|
9939
9825
|
*/
|
|
9940
|
-
function
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
if (
|
|
9946
|
-
|
|
9826
|
+
function enforceInvariant(state) {
|
|
9827
|
+
const result = { ...state };
|
|
9828
|
+
let foundNonFresh = false;
|
|
9829
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
9830
|
+
const s = result[phase];
|
|
9831
|
+
if (s === 'fresh')
|
|
9832
|
+
continue;
|
|
9833
|
+
if (!foundNonFresh) {
|
|
9834
|
+
foundNonFresh = true;
|
|
9835
|
+
// First non-fresh: if stale, promote to pending
|
|
9836
|
+
if (s === 'stale') {
|
|
9837
|
+
result[phase] = 'pending';
|
|
9838
|
+
}
|
|
9839
|
+
// pending, running, failed stay as-is
|
|
9840
|
+
}
|
|
9841
|
+
else {
|
|
9842
|
+
// Subsequent non-fresh: must not be pending or running
|
|
9843
|
+
if (s === 'pending') {
|
|
9844
|
+
result[phase] = 'stale';
|
|
9845
|
+
}
|
|
9846
|
+
// running in non-first position would be a bug, but don't mask it
|
|
9947
9847
|
}
|
|
9948
9848
|
}
|
|
9949
|
-
return
|
|
9849
|
+
return result;
|
|
9950
9850
|
}
|
|
9851
|
+
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
9951
9852
|
/**
|
|
9952
|
-
*
|
|
9953
|
-
*
|
|
9954
|
-
*
|
|
9955
|
-
* filter → computeEffectiveStaleness → selectCandidate → return path
|
|
9956
|
-
*
|
|
9957
|
-
* @param candidates - Array with node, meta, and stalenessSeconds.
|
|
9958
|
-
* @param depthWeight - Depth weighting exponent from config.
|
|
9959
|
-
* @returns The stalest candidate's metaPath, or null if none are stale.
|
|
9853
|
+
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
9854
|
+
* Triggers: _structureHash change, _steer change, _architect change,
|
|
9855
|
+
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
9960
9856
|
*/
|
|
9961
|
-
function
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9857
|
+
function invalidateArchitect(state) {
|
|
9858
|
+
return enforceInvariant({
|
|
9859
|
+
architect: state.architect === 'failed' ? 'failed' : 'pending',
|
|
9860
|
+
builder: state.builder === 'fresh' ? 'stale' : state.builder,
|
|
9861
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9862
|
+
});
|
|
9965
9863
|
}
|
|
9966
|
-
|
|
9967
9864
|
/**
|
|
9968
|
-
*
|
|
9969
|
-
*
|
|
9970
|
-
*
|
|
9865
|
+
* Builder invalidated (scope mtime or cross-ref _content change):
|
|
9866
|
+
* builder → pending; critic → stale.
|
|
9867
|
+
* Only applies when architect is fresh; otherwise, builder stays stale.
|
|
9971
9868
|
*/
|
|
9869
|
+
function invalidateBuilder(state) {
|
|
9870
|
+
if (state.architect !== 'fresh') {
|
|
9871
|
+
// Architect is not fresh — builder stays stale (or whatever it is)
|
|
9872
|
+
return enforceInvariant({
|
|
9873
|
+
...state,
|
|
9874
|
+
builder: state.builder === 'fresh' || state.builder === 'stale'
|
|
9875
|
+
? 'stale'
|
|
9876
|
+
: state.builder,
|
|
9877
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9878
|
+
});
|
|
9879
|
+
}
|
|
9880
|
+
return enforceInvariant({
|
|
9881
|
+
...state,
|
|
9882
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
9883
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9884
|
+
});
|
|
9885
|
+
}
|
|
9886
|
+
// ── Phase success transitions ──────────────────────────────────────────
|
|
9972
9887
|
/**
|
|
9973
|
-
*
|
|
9974
|
-
*
|
|
9975
|
-
* @param step - Which synthesis step failed.
|
|
9976
|
-
* @param err - The caught error value.
|
|
9977
|
-
* @param code - Error classification code.
|
|
9978
|
-
* @returns A structured MetaError.
|
|
9888
|
+
* Architect completes successfully.
|
|
9889
|
+
* architect → fresh; builder → pending; critic → stale.
|
|
9979
9890
|
*/
|
|
9980
|
-
function
|
|
9891
|
+
function architectSuccess(state) {
|
|
9892
|
+
return enforceInvariant({
|
|
9893
|
+
architect: 'fresh',
|
|
9894
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
9895
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9896
|
+
});
|
|
9897
|
+
}
|
|
9898
|
+
/**
|
|
9899
|
+
* Builder completes successfully.
|
|
9900
|
+
* builder → fresh; critic → pending.
|
|
9901
|
+
*/
|
|
9902
|
+
function builderSuccess(state) {
|
|
9903
|
+
return enforceInvariant({
|
|
9904
|
+
...state,
|
|
9905
|
+
builder: 'fresh',
|
|
9906
|
+
critic: state.critic === 'failed' ? 'failed' : 'pending',
|
|
9907
|
+
});
|
|
9908
|
+
}
|
|
9909
|
+
/**
|
|
9910
|
+
* Critic completes successfully.
|
|
9911
|
+
* critic → fresh. Meta becomes fully fresh.
|
|
9912
|
+
*/
|
|
9913
|
+
function criticSuccess(state) {
|
|
9914
|
+
return enforceInvariant({
|
|
9915
|
+
...state,
|
|
9916
|
+
critic: 'fresh',
|
|
9917
|
+
});
|
|
9918
|
+
}
|
|
9919
|
+
// ── Failure transition ─────────────────────────────────────────────────
|
|
9920
|
+
/**
|
|
9921
|
+
* A phase fails (error, timeout, or abort).
|
|
9922
|
+
* Target phase → failed; upstream and downstream unchanged.
|
|
9923
|
+
*/
|
|
9924
|
+
function phaseFailed(state, phase) {
|
|
9925
|
+
return enforceInvariant({
|
|
9926
|
+
...state,
|
|
9927
|
+
[phase]: 'failed',
|
|
9928
|
+
});
|
|
9929
|
+
}
|
|
9930
|
+
// ── Surgical retry ─────────────────────────────────────────────────────
|
|
9931
|
+
/**
|
|
9932
|
+
* Retry a failed phase: failed → pending.
|
|
9933
|
+
* Only valid when the phase is currently failed.
|
|
9934
|
+
*/
|
|
9935
|
+
function retryPhase(state, phase) {
|
|
9936
|
+
if (state[phase] !== 'failed')
|
|
9937
|
+
return state;
|
|
9938
|
+
return enforceInvariant({
|
|
9939
|
+
...state,
|
|
9940
|
+
[phase]: 'pending',
|
|
9941
|
+
});
|
|
9942
|
+
}
|
|
9943
|
+
/**
|
|
9944
|
+
* Retry all failed phases: each failed phase → pending.
|
|
9945
|
+
* Used by scheduler ticks and queue reads to auto-promote failed phases.
|
|
9946
|
+
*/
|
|
9947
|
+
function retryAllFailed(state) {
|
|
9948
|
+
let result = state;
|
|
9949
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
9950
|
+
if (result[phase] === 'failed') {
|
|
9951
|
+
result = retryPhase(result, phase);
|
|
9952
|
+
}
|
|
9953
|
+
}
|
|
9954
|
+
return result;
|
|
9955
|
+
}
|
|
9956
|
+
// ── Running transition ─────────────────────────────────────────────────
|
|
9957
|
+
/**
|
|
9958
|
+
* Mark a phase as running (scheduler picks it).
|
|
9959
|
+
*/
|
|
9960
|
+
function phaseRunning(state, phase) {
|
|
9981
9961
|
return {
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
message: err instanceof Error ? err.message : String(err),
|
|
9962
|
+
...state,
|
|
9963
|
+
[phase]: 'running',
|
|
9985
9964
|
};
|
|
9986
9965
|
}
|
|
9966
|
+
// ── Query helpers ──────────────────────────────────────────────────────
|
|
9967
|
+
/**
|
|
9968
|
+
* Get the owed phase: first non-fresh phase in pipeline order, or null.
|
|
9969
|
+
*/
|
|
9970
|
+
function getOwedPhase(state) {
|
|
9971
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
9972
|
+
if (state[phase] !== 'fresh')
|
|
9973
|
+
return phase;
|
|
9974
|
+
}
|
|
9975
|
+
return null;
|
|
9976
|
+
}
|
|
9977
|
+
/**
|
|
9978
|
+
* Check if a meta is fully fresh (all phases fresh).
|
|
9979
|
+
*/
|
|
9980
|
+
function isFullyFresh(state) {
|
|
9981
|
+
return (state.architect === 'fresh' &&
|
|
9982
|
+
state.builder === 'fresh' &&
|
|
9983
|
+
state.critic === 'fresh');
|
|
9984
|
+
}
|
|
9985
|
+
/**
|
|
9986
|
+
* Get the scheduler priority band for a meta's owed phase.
|
|
9987
|
+
* 1 = critic (highest), 2 = builder, 3 = architect, null = fully fresh.
|
|
9988
|
+
*/
|
|
9989
|
+
function getPriorityBand(state) {
|
|
9990
|
+
const owed = getOwedPhase(state);
|
|
9991
|
+
if (!owed)
|
|
9992
|
+
return null;
|
|
9993
|
+
if (owed === 'critic')
|
|
9994
|
+
return 1;
|
|
9995
|
+
if (owed === 'builder')
|
|
9996
|
+
return 2;
|
|
9997
|
+
return 3;
|
|
9998
|
+
}
|
|
9999
|
+
|
|
10000
|
+
/**
|
|
10001
|
+
* Backward-compatible derivation of _phaseState from existing meta fields.
|
|
10002
|
+
*
|
|
10003
|
+
* When a meta is loaded from disk without _phaseState, this reconstructs
|
|
10004
|
+
* the phase state from _content, _builder, _state, _error.step, and
|
|
10005
|
+
* the architect-invalidating inputs.
|
|
10006
|
+
*
|
|
10007
|
+
* @module phaseState/derivePhaseState
|
|
10008
|
+
*/
|
|
10009
|
+
/**
|
|
10010
|
+
* Derive _phaseState from existing meta fields.
|
|
10011
|
+
*
|
|
10012
|
+
* If the meta already has _phaseState, returns it as-is.
|
|
10013
|
+
*
|
|
10014
|
+
* Otherwise, reconstructs from available fields:
|
|
10015
|
+
* - Never-synthesized meta (no _content, no _builder): all phases start pending/stale.
|
|
10016
|
+
* - Errored meta: the failed phase is mapped from _error.step.
|
|
10017
|
+
* - Mid-cycle meta with cached _builder but no _content: builder pending.
|
|
10018
|
+
* - Fully-fresh meta: all phases fresh.
|
|
10019
|
+
* - Meta with stale architect inputs: architect pending, downstream stale.
|
|
10020
|
+
*
|
|
10021
|
+
* @param meta - The meta.json content.
|
|
10022
|
+
* @param inputs - Optional derivation inputs. If not provided, a simpler
|
|
10023
|
+
* heuristic is used (no architect invalidation check).
|
|
10024
|
+
* @returns The derived PhaseState.
|
|
10025
|
+
*/
|
|
10026
|
+
function derivePhaseState(meta, inputs) {
|
|
10027
|
+
// Already has _phaseState — use it
|
|
10028
|
+
if (meta._phaseState)
|
|
10029
|
+
return meta._phaseState;
|
|
10030
|
+
// Check for errors first — _error.step maps directly to failed phase
|
|
10031
|
+
if (meta._error) {
|
|
10032
|
+
const failedPhase = meta._error.step;
|
|
10033
|
+
const state = freshPhaseState();
|
|
10034
|
+
state[failedPhase] = 'failed';
|
|
10035
|
+
// If architect failed and no _builder, downstream is stale
|
|
10036
|
+
if (failedPhase === 'architect') {
|
|
10037
|
+
if (!meta._builder) {
|
|
10038
|
+
state.builder = 'stale';
|
|
10039
|
+
state.critic = 'stale';
|
|
10040
|
+
}
|
|
10041
|
+
}
|
|
10042
|
+
// If builder failed, critic is stale
|
|
10043
|
+
if (failedPhase === 'builder') {
|
|
10044
|
+
state.critic = 'stale';
|
|
10045
|
+
}
|
|
10046
|
+
return state;
|
|
10047
|
+
}
|
|
10048
|
+
// Never synthesized: no _content AND no _builder (and no error)
|
|
10049
|
+
if (!meta._content && !meta._builder) {
|
|
10050
|
+
return initialPhaseState();
|
|
10051
|
+
}
|
|
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
|
+
// Has _builder but no _content: builder is pending
|
|
10078
|
+
if (meta._builder && !meta._content) {
|
|
10079
|
+
return {
|
|
10080
|
+
architect: 'fresh',
|
|
10081
|
+
builder: 'pending',
|
|
10082
|
+
critic: 'stale',
|
|
10083
|
+
};
|
|
10084
|
+
}
|
|
10085
|
+
// Has _content but no _feedback: critic is pending
|
|
10086
|
+
if (meta._content && !meta._feedback) {
|
|
10087
|
+
return {
|
|
10088
|
+
architect: 'fresh',
|
|
10089
|
+
builder: 'fresh',
|
|
10090
|
+
critic: 'pending',
|
|
10091
|
+
};
|
|
10092
|
+
}
|
|
10093
|
+
// Default: fully fresh
|
|
10094
|
+
return freshPhaseState();
|
|
10095
|
+
}
|
|
9987
10096
|
|
|
9988
10097
|
/**
|
|
9989
10098
|
* Compute a structure hash from a sorted file listing.
|
|
@@ -10006,86 +10115,296 @@ function computeStructureHash(filePaths) {
|
|
|
10006
10115
|
}
|
|
10007
10116
|
|
|
10008
10117
|
/**
|
|
10009
|
-
*
|
|
10010
|
-
*
|
|
10011
|
-
* @module orchestrator/finalizeCycle
|
|
10012
|
-
*/
|
|
10013
|
-
/** Finalize a cycle using lock staging: write to .lock → copy to meta.json + archive → delete .lock. */
|
|
10014
|
-
async function finalizeCycle(opts) {
|
|
10015
|
-
const lockPath = join(opts.metaPath, '.lock');
|
|
10016
|
-
const metaJsonPath = join(opts.metaPath, 'meta.json');
|
|
10017
|
-
// Stage: write merged result to .lock (sequential — ordering matters)
|
|
10018
|
-
const updated = await mergeAndWrite({
|
|
10019
|
-
metaPath: opts.metaPath,
|
|
10020
|
-
current: opts.current,
|
|
10021
|
-
architect: opts.architect,
|
|
10022
|
-
builder: opts.builder,
|
|
10023
|
-
critic: opts.critic,
|
|
10024
|
-
builderOutput: opts.builderOutput,
|
|
10025
|
-
feedback: opts.feedback,
|
|
10026
|
-
structureHash: opts.structureHash,
|
|
10027
|
-
synthesisCount: opts.synthesisCount,
|
|
10028
|
-
error: opts.error,
|
|
10029
|
-
architectTokens: opts.architectTokens,
|
|
10030
|
-
builderTokens: opts.builderTokens,
|
|
10031
|
-
criticTokens: opts.criticTokens,
|
|
10032
|
-
outputPath: lockPath,
|
|
10033
|
-
state: opts.state,
|
|
10034
|
-
stateOnly: opts.stateOnly,
|
|
10035
|
-
});
|
|
10036
|
-
// Commit: copy .lock → meta.json
|
|
10037
|
-
await copyFile(lockPath, metaJsonPath);
|
|
10038
|
-
// Archive + prune from the committed meta.json (sequential)
|
|
10039
|
-
await createSnapshot(opts.metaPath, updated);
|
|
10040
|
-
await pruneArchive(opts.metaPath, opts.config.maxArchive);
|
|
10041
|
-
// .lock is cleaned up by the finally block (releaseLock)
|
|
10042
|
-
return updated;
|
|
10043
|
-
}
|
|
10044
|
-
|
|
10045
|
-
/**
|
|
10046
|
-
* Parse subprocess outputs for each synthesis step.
|
|
10118
|
+
* Per-tick invalidation pass.
|
|
10047
10119
|
*
|
|
10048
|
-
* -
|
|
10049
|
-
*
|
|
10050
|
-
* - Critic: returns text \> _feedback
|
|
10120
|
+
* Computes architect-invalidating and builder-invalidating inputs for a meta,
|
|
10121
|
+
* then applies the cascade to update _phaseState.
|
|
10051
10122
|
*
|
|
10052
|
-
* @module
|
|
10123
|
+
* @module phaseState/invalidate
|
|
10053
10124
|
*/
|
|
10054
10125
|
/**
|
|
10055
|
-
*
|
|
10126
|
+
* Compute invalidation inputs and apply cascade for a single meta.
|
|
10056
10127
|
*
|
|
10057
|
-
* @param
|
|
10058
|
-
* @
|
|
10128
|
+
* @param meta - Current meta.json content with existing _phaseState.
|
|
10129
|
+
* @param scopeFiles - Sorted file list from scope.
|
|
10130
|
+
* @param config - MetaConfig for architectEvery.
|
|
10131
|
+
* @param node - MetaNode for archive access.
|
|
10132
|
+
* @param crossRefMetas - Map of cross-ref owner paths to their current _content.
|
|
10133
|
+
* @param archiveCrossRefContent - Map of cross-ref owner paths to their archived _content.
|
|
10134
|
+
* @returns Updated phase state and invalidation details.
|
|
10059
10135
|
*/
|
|
10060
|
-
function
|
|
10061
|
-
|
|
10136
|
+
async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas, archiveCrossRefContent) {
|
|
10137
|
+
let phaseState = meta._phaseState ?? {
|
|
10138
|
+
architect: 'fresh',
|
|
10139
|
+
builder: 'fresh',
|
|
10140
|
+
critic: 'fresh',
|
|
10141
|
+
};
|
|
10142
|
+
// ── Architect-level inputs ──
|
|
10143
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
10144
|
+
const structureChanged = structureHash !== meta._structureHash;
|
|
10145
|
+
const latestArchive = await readLatestArchive(node.metaPath);
|
|
10146
|
+
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10147
|
+
// _architect change: compare current vs. archive
|
|
10148
|
+
const architectChanged = latestArchive
|
|
10149
|
+
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
10150
|
+
: Boolean(meta._architect);
|
|
10151
|
+
// _crossRefs declaration change
|
|
10152
|
+
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
10153
|
+
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
10154
|
+
.slice()
|
|
10155
|
+
.sort()
|
|
10156
|
+
.join(',');
|
|
10157
|
+
const crossRefsDeclChanged = latestArchive
|
|
10158
|
+
? currentRefs !== archiveRefs
|
|
10159
|
+
: currentRefs.length > 0;
|
|
10160
|
+
const architectInvalidators = [];
|
|
10161
|
+
if (structureChanged) {
|
|
10162
|
+
if (meta._state !== undefined) {
|
|
10163
|
+
// Progressive entity: new files → builder only (cursor handles incremental)
|
|
10164
|
+
phaseState = invalidateBuilder(phaseState);
|
|
10165
|
+
}
|
|
10166
|
+
else {
|
|
10167
|
+
architectInvalidators.push('structureHash');
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
10170
|
+
if (steerChanged)
|
|
10171
|
+
architectInvalidators.push('steer');
|
|
10172
|
+
if (architectChanged)
|
|
10173
|
+
architectInvalidators.push('_architect');
|
|
10174
|
+
if (crossRefsDeclChanged)
|
|
10175
|
+
architectInvalidators.push('_crossRefs');
|
|
10176
|
+
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
10177
|
+
architectInvalidators.push('architectEvery');
|
|
10178
|
+
}
|
|
10179
|
+
// First-run check: no _builder means architect must run
|
|
10180
|
+
const firstRun = !meta._builder;
|
|
10181
|
+
if (architectInvalidators.length > 0 || firstRun) {
|
|
10182
|
+
phaseState = invalidateArchitect(phaseState);
|
|
10183
|
+
}
|
|
10184
|
+
// ── 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
|
+
// Cross-ref _content change (builder-invalidating)
|
|
10190
|
+
let crossRefContentChanged = false;
|
|
10191
|
+
return {
|
|
10192
|
+
phaseState,
|
|
10193
|
+
architectInvalidators,
|
|
10194
|
+
stalenessInputs: {
|
|
10195
|
+
structureHash,
|
|
10196
|
+
steerChanged,
|
|
10197
|
+
architectChanged,
|
|
10198
|
+
crossRefsDeclChanged,
|
|
10199
|
+
scopeMtimeMax,
|
|
10200
|
+
crossRefContentChanged,
|
|
10201
|
+
},
|
|
10202
|
+
structureHash,
|
|
10203
|
+
steerChanged,
|
|
10204
|
+
};
|
|
10062
10205
|
}
|
|
10206
|
+
|
|
10063
10207
|
/**
|
|
10064
|
-
*
|
|
10208
|
+
* Weighted staleness formula for candidate selection.
|
|
10065
10209
|
*
|
|
10066
|
-
*
|
|
10210
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10067
10211
|
*
|
|
10068
|
-
* @
|
|
10069
|
-
* @returns Parsed builder output with content and structured fields.
|
|
10212
|
+
* @module scheduling/weightedFormula
|
|
10070
10213
|
*/
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10214
|
+
/**
|
|
10215
|
+
* Compute effective staleness for a set of candidates.
|
|
10216
|
+
*
|
|
10217
|
+
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
10218
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10219
|
+
*
|
|
10220
|
+
* Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
|
|
10221
|
+
* metas to tune how much their tree position affects scheduling.
|
|
10222
|
+
*
|
|
10223
|
+
* @param candidates - Array of \{ node, meta, actualStaleness \}.
|
|
10224
|
+
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
10225
|
+
* @returns Same array with effectiveStaleness computed.
|
|
10226
|
+
*/
|
|
10227
|
+
function computeEffectiveStaleness(candidates, depthWeight) {
|
|
10228
|
+
if (candidates.length === 0)
|
|
10229
|
+
return [];
|
|
10230
|
+
// Get depth for each candidate: use _depth override or tree depth
|
|
10231
|
+
const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
|
|
10232
|
+
// Normalize: shift so minimum becomes 0
|
|
10233
|
+
const minDepth = Math.min(...depths);
|
|
10234
|
+
const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
|
|
10235
|
+
return candidates.map((c, i) => {
|
|
10236
|
+
const emphasis = c.meta._emphasis ?? 1;
|
|
10237
|
+
return {
|
|
10238
|
+
...c,
|
|
10239
|
+
effectiveStaleness: c.actualStaleness *
|
|
10240
|
+
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
10241
|
+
};
|
|
10242
|
+
});
|
|
10243
|
+
}
|
|
10244
|
+
|
|
10245
|
+
/**
|
|
10246
|
+
* Corpus-wide phase scheduler.
|
|
10247
|
+
*
|
|
10248
|
+
* Selects the highest-priority ready phase across all metas.
|
|
10249
|
+
* Priority: critic (band 1) \> builder (band 2) \> architect (band 3).
|
|
10250
|
+
* Tiebreak within band: weighted staleness (§3.9).
|
|
10251
|
+
*
|
|
10252
|
+
* @module phaseState/phaseScheduler
|
|
10253
|
+
*/
|
|
10254
|
+
/**
|
|
10255
|
+
* Build phase candidates from listMetas entries.
|
|
10256
|
+
*
|
|
10257
|
+
* Derives phase state, auto-retries failed phases, and applies Tier 1
|
|
10258
|
+
* cheap-invalidation (no I/O) for metas with persisted _phaseState.
|
|
10259
|
+
* Used by orchestratePhase, queue route, and status route.
|
|
10260
|
+
*/
|
|
10261
|
+
function buildPhaseCandidates(entries, architectEvery) {
|
|
10262
|
+
return entries.map((entry) => {
|
|
10263
|
+
let ps = retryAllFailed(derivePhaseState(entry.meta));
|
|
10264
|
+
// Tier 1 cheap invalidation for metas with persisted _phaseState
|
|
10265
|
+
if (entry.meta._phaseState) {
|
|
10266
|
+
const needsArchitect = !entry.meta._builder ||
|
|
10267
|
+
(entry.meta._synthesisCount ?? 0) >= architectEvery;
|
|
10268
|
+
if (needsArchitect && ps.architect === 'fresh') {
|
|
10269
|
+
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
10270
|
+
}
|
|
10271
|
+
}
|
|
10272
|
+
return {
|
|
10273
|
+
node: entry.node,
|
|
10274
|
+
meta: entry.meta,
|
|
10275
|
+
phaseState: ps,
|
|
10276
|
+
actualStaleness: entry.stalenessSeconds,
|
|
10277
|
+
locked: entry.locked,
|
|
10278
|
+
disabled: entry.disabled,
|
|
10279
|
+
};
|
|
10280
|
+
});
|
|
10281
|
+
}
|
|
10282
|
+
/**
|
|
10283
|
+
* Rank all eligible phase candidates by priority.
|
|
10284
|
+
*
|
|
10285
|
+
* Filters to pending phases, computes effective staleness, and sorts by
|
|
10286
|
+
* band (ascending: critic first) then effective staleness (descending).
|
|
10287
|
+
*
|
|
10288
|
+
* Used by selectPhaseCandidate (returns first) and the queue route (returns all).
|
|
10289
|
+
*/
|
|
10290
|
+
function rankPhaseCandidates(metas, depthWeight) {
|
|
10291
|
+
// Filter to metas with a pending (scheduler-eligible) phase
|
|
10292
|
+
const eligible = metas.filter((m) => {
|
|
10293
|
+
if (m.locked)
|
|
10294
|
+
return false;
|
|
10295
|
+
if (m.disabled && !m.isOverride)
|
|
10296
|
+
return false;
|
|
10297
|
+
const owed = getOwedPhase(m.phaseState);
|
|
10298
|
+
if (!owed)
|
|
10299
|
+
return false;
|
|
10300
|
+
return m.phaseState[owed] === 'pending';
|
|
10301
|
+
});
|
|
10302
|
+
if (eligible.length === 0)
|
|
10303
|
+
return [];
|
|
10304
|
+
// Compute effective staleness for tiebreaking
|
|
10305
|
+
const withStaleness = computeEffectiveStaleness(eligible.map((m) => ({
|
|
10306
|
+
node: m.node,
|
|
10307
|
+
meta: m.meta,
|
|
10308
|
+
actualStaleness: m.actualStaleness,
|
|
10309
|
+
})), depthWeight);
|
|
10310
|
+
// Build candidates with band info
|
|
10311
|
+
const candidates = withStaleness.map((ws, i) => {
|
|
10312
|
+
const m = eligible[i];
|
|
10313
|
+
const owedPhase = getOwedPhase(m.phaseState);
|
|
10314
|
+
return {
|
|
10315
|
+
node: ws.node,
|
|
10316
|
+
meta: ws.meta,
|
|
10317
|
+
phaseState: m.phaseState,
|
|
10318
|
+
owedPhase,
|
|
10319
|
+
band: getPriorityBand(m.phaseState),
|
|
10320
|
+
actualStaleness: ws.actualStaleness,
|
|
10321
|
+
effectiveStaleness: ws.effectiveStaleness,
|
|
10322
|
+
};
|
|
10323
|
+
});
|
|
10324
|
+
// Sort by band (ascending = critic first) then effective staleness (descending)
|
|
10325
|
+
candidates.sort((a, b) => {
|
|
10326
|
+
if (a.band !== b.band)
|
|
10327
|
+
return a.band - b.band;
|
|
10328
|
+
return b.effectiveStaleness - a.effectiveStaleness;
|
|
10329
|
+
});
|
|
10330
|
+
return candidates;
|
|
10331
|
+
}
|
|
10332
|
+
/**
|
|
10333
|
+
* Select the best phase candidate across the corpus.
|
|
10334
|
+
*
|
|
10335
|
+
* @param metas - Array of (node, meta, phaseState, stalenessSeconds) tuples.
|
|
10336
|
+
* @param depthWeight - Config depthWeight for staleness tiebreak.
|
|
10337
|
+
* @returns The winning candidate, or null if no phase is ready.
|
|
10338
|
+
*/
|
|
10339
|
+
function selectPhaseCandidate(metas, depthWeight) {
|
|
10340
|
+
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
10341
|
+
}
|
|
10342
|
+
|
|
10343
|
+
/**
|
|
10344
|
+
* Shared error utilities.
|
|
10345
|
+
*
|
|
10346
|
+
* @module errors
|
|
10347
|
+
*/
|
|
10348
|
+
/**
|
|
10349
|
+
* Wrap an unknown caught value into a MetaError.
|
|
10350
|
+
*
|
|
10351
|
+
* @param step - Which synthesis step failed.
|
|
10352
|
+
* @param err - The caught error value.
|
|
10353
|
+
* @param code - Error classification code.
|
|
10354
|
+
* @returns A structured MetaError.
|
|
10355
|
+
*/
|
|
10356
|
+
function toMetaError(step, err, code = 'FAILED') {
|
|
10357
|
+
return {
|
|
10358
|
+
step,
|
|
10359
|
+
code,
|
|
10360
|
+
message: err instanceof Error ? err.message : String(err),
|
|
10361
|
+
};
|
|
10362
|
+
}
|
|
10363
|
+
|
|
10364
|
+
/**
|
|
10365
|
+
* Parse subprocess outputs for each synthesis step.
|
|
10366
|
+
*
|
|
10367
|
+
* - Architect: returns text \> _builder
|
|
10368
|
+
* - Builder: returns JSON \> _content + structured fields
|
|
10369
|
+
* - Critic: returns text \> _feedback
|
|
10370
|
+
*
|
|
10371
|
+
* @module orchestrator/parseOutput
|
|
10372
|
+
*/
|
|
10373
|
+
/**
|
|
10374
|
+
* Parse architect output. The architect returns a task brief as text.
|
|
10375
|
+
*
|
|
10376
|
+
* @param output - Raw subprocess output.
|
|
10377
|
+
* @returns The task brief string.
|
|
10378
|
+
*/
|
|
10379
|
+
function parseArchitectOutput(output) {
|
|
10380
|
+
return output.trim();
|
|
10381
|
+
}
|
|
10382
|
+
/**
|
|
10383
|
+
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
10384
|
+
*
|
|
10385
|
+
* Attempts JSON parse first. If that fails, treats the entire output as _content.
|
|
10386
|
+
*
|
|
10387
|
+
* @param output - Raw subprocess output.
|
|
10388
|
+
* @returns Parsed builder output with content and structured fields.
|
|
10389
|
+
*/
|
|
10390
|
+
function parseBuilderOutput(output) {
|
|
10391
|
+
const trimmed = output.trim();
|
|
10392
|
+
// Strategy 1: Try to parse the entire output as JSON directly
|
|
10393
|
+
const direct = tryParseJson(trimmed);
|
|
10394
|
+
if (direct)
|
|
10395
|
+
return direct;
|
|
10396
|
+
// Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
|
|
10397
|
+
const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
10398
|
+
const fenceMatches = [];
|
|
10399
|
+
let match;
|
|
10400
|
+
while ((match = fencePattern.exec(trimmed)) !== null) {
|
|
10401
|
+
fenceMatches.push(match[1].trim());
|
|
10402
|
+
}
|
|
10403
|
+
// Try last fence first (most likely to be the actual output)
|
|
10404
|
+
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
10405
|
+
const result = tryParseJson(fenceMatches[i]);
|
|
10406
|
+
if (result)
|
|
10407
|
+
return result;
|
|
10089
10408
|
}
|
|
10090
10409
|
// Strategy 3: Find outermost { ... } braces
|
|
10091
10410
|
const firstBrace = trimmed.indexOf('{');
|
|
@@ -10121,730 +10440,22 @@ function tryParseJson(str) {
|
|
|
10121
10440
|
for (const [key, value] of Object.entries(parsed)) {
|
|
10122
10441
|
if (!key.startsWith('_') && key !== 'content') {
|
|
10123
10442
|
fields[key] = value;
|
|
10124
|
-
}
|
|
10125
|
-
}
|
|
10126
|
-
return { content, fields, ...(state !== undefined ? { state } : {}) };
|
|
10127
|
-
}
|
|
10128
|
-
catch {
|
|
10129
|
-
return null;
|
|
10130
|
-
}
|
|
10131
|
-
}
|
|
10132
|
-
/**
|
|
10133
|
-
* Parse critic output. The critic returns evaluation text.
|
|
10134
|
-
*
|
|
10135
|
-
* @param output - Raw subprocess output.
|
|
10136
|
-
* @returns The feedback string.
|
|
10137
|
-
*/
|
|
10138
|
-
function parseCriticOutput(output) {
|
|
10139
|
-
return output.trim();
|
|
10140
|
-
}
|
|
10141
|
-
|
|
10142
|
-
/**
|
|
10143
|
-
* Timeout recovery — salvage partial builder state after a SpawnTimeoutError.
|
|
10144
|
-
*
|
|
10145
|
-
* @module orchestrator/timeoutRecovery
|
|
10146
|
-
*/
|
|
10147
|
-
/**
|
|
10148
|
-
* Attempt to recover partial state from a timed-out builder spawn.
|
|
10149
|
-
*
|
|
10150
|
-
* Returns an {@link OrchestrateResult} if state was salvaged, or `null`
|
|
10151
|
-
* if the caller should fall through to a hard failure.
|
|
10152
|
-
*/
|
|
10153
|
-
async function attemptTimeoutRecovery(opts) {
|
|
10154
|
-
const { err, currentMeta, metaPath, config, builderBrief, structureHash, synthesisCount, } = opts;
|
|
10155
|
-
let partialOutput = null;
|
|
10156
|
-
try {
|
|
10157
|
-
const raw = await readFile(err.outputPath, 'utf8');
|
|
10158
|
-
partialOutput = parseBuilderOutput(raw);
|
|
10159
|
-
}
|
|
10160
|
-
catch {
|
|
10161
|
-
// Could not read partial output — fall through to hard failure
|
|
10162
|
-
}
|
|
10163
|
-
if (partialOutput?.state !== undefined) {
|
|
10164
|
-
const currentState = JSON.stringify(currentMeta._state);
|
|
10165
|
-
const newState = JSON.stringify(partialOutput.state);
|
|
10166
|
-
if (newState !== currentState) {
|
|
10167
|
-
const timeoutError = {
|
|
10168
|
-
step: 'builder',
|
|
10169
|
-
code: 'TIMEOUT',
|
|
10170
|
-
message: err.message,
|
|
10171
|
-
};
|
|
10172
|
-
await finalizeCycle({
|
|
10173
|
-
metaPath,
|
|
10174
|
-
current: currentMeta,
|
|
10175
|
-
config,
|
|
10176
|
-
architect: currentMeta._architect ?? '',
|
|
10177
|
-
builder: builderBrief,
|
|
10178
|
-
critic: currentMeta._critic ?? '',
|
|
10179
|
-
builderOutput: null,
|
|
10180
|
-
feedback: null,
|
|
10181
|
-
structureHash,
|
|
10182
|
-
synthesisCount,
|
|
10183
|
-
error: timeoutError,
|
|
10184
|
-
state: partialOutput.state,
|
|
10185
|
-
stateOnly: true,
|
|
10186
|
-
});
|
|
10187
|
-
return {
|
|
10188
|
-
synthesized: true,
|
|
10189
|
-
metaPath,
|
|
10190
|
-
error: timeoutError,
|
|
10191
|
-
};
|
|
10192
|
-
}
|
|
10193
|
-
}
|
|
10194
|
-
return null;
|
|
10195
|
-
}
|
|
10196
|
-
|
|
10197
|
-
/**
|
|
10198
|
-
* Single-node synthesis pipeline — architect, builder, critic.
|
|
10199
|
-
*
|
|
10200
|
-
* @module orchestrator/synthesizeNode
|
|
10201
|
-
*/
|
|
10202
|
-
/** Run the architect/builder/critic pipeline on a single node. */
|
|
10203
|
-
async function synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger) {
|
|
10204
|
-
// Step 5-6: Steer change detection
|
|
10205
|
-
const latestArchive = await readLatestArchive(node.metaPath);
|
|
10206
|
-
const steerChanged = hasSteerChanged(currentMeta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10207
|
-
// Step 7: Compute context (includes scope files and delta files)
|
|
10208
|
-
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10209
|
-
// Skip empty-scope entities that have no prior content.
|
|
10210
|
-
// Without scope files, child metas, or cross-refs there is nothing for
|
|
10211
|
-
// the architect/builder to work with and the cycle will either time out
|
|
10212
|
-
// or produce empty output.
|
|
10213
|
-
const hasScope = ctx.scopeFiles.length > 0 ||
|
|
10214
|
-
Object.keys(ctx.childMetas).length > 0 ||
|
|
10215
|
-
Object.keys(ctx.crossRefMetas).length > 0;
|
|
10216
|
-
if (!hasScope && !currentMeta._content) {
|
|
10217
|
-
// Bump _generatedAt so this entity doesn't keep winning the staleness
|
|
10218
|
-
// race every cycle. It will be re-evaluated when files appear.
|
|
10219
|
-
// Uses lock-staging for atomic write consistency.
|
|
10220
|
-
currentMeta._generatedAt = new Date().toISOString();
|
|
10221
|
-
const lockPath = join(node.metaPath, '.lock');
|
|
10222
|
-
const metaJsonPath = join(node.metaPath, 'meta.json');
|
|
10223
|
-
await writeFile(lockPath, JSON.stringify(currentMeta, null, 2));
|
|
10224
|
-
await copyFile(lockPath, metaJsonPath);
|
|
10225
|
-
logger?.debug({ path: node.ownerPath }, 'Skipping empty-scope entity');
|
|
10226
|
-
return { synthesized: false };
|
|
10227
|
-
}
|
|
10228
|
-
// Step 5 (deferred): Structure hash from context scope files
|
|
10229
|
-
const newStructureHash = computeStructureHash(ctx.scopeFiles);
|
|
10230
|
-
const structureChanged = newStructureHash !== currentMeta._structureHash;
|
|
10231
|
-
// Step 8: Architect (conditional)
|
|
10232
|
-
const architectTriggered = isArchitectTriggered(currentMeta, structureChanged, steerChanged, config.architectEvery);
|
|
10233
|
-
let builderBrief = currentMeta._builder ?? '';
|
|
10234
|
-
let synthesisCount = currentMeta._synthesisCount ?? 0;
|
|
10235
|
-
let stepError = null;
|
|
10236
|
-
let architectTokens;
|
|
10237
|
-
let builderTokens;
|
|
10238
|
-
let criticTokens;
|
|
10239
|
-
// Shared base options for all finalizeCycle calls.
|
|
10240
|
-
// Note: synthesisCount is excluded because it mutates during the pipeline.
|
|
10241
|
-
const baseFinalizeOptions = {
|
|
10242
|
-
metaPath: node.metaPath,
|
|
10243
|
-
current: currentMeta,
|
|
10244
|
-
config,
|
|
10245
|
-
architect: currentMeta._architect ?? '',
|
|
10246
|
-
critic: currentMeta._critic ?? '',
|
|
10247
|
-
structureHash: newStructureHash,
|
|
10248
|
-
};
|
|
10249
|
-
if (architectTriggered) {
|
|
10250
|
-
try {
|
|
10251
|
-
await onProgress?.({
|
|
10252
|
-
type: 'phase_start',
|
|
10253
|
-
path: node.ownerPath,
|
|
10254
|
-
phase: 'architect',
|
|
10255
|
-
});
|
|
10256
|
-
const phaseStart = Date.now();
|
|
10257
|
-
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
10258
|
-
const architectResult = await executor.spawn(architectTask, {
|
|
10259
|
-
thinking: config.thinking,
|
|
10260
|
-
timeout: config.architectTimeout,
|
|
10261
|
-
label: 'meta-architect',
|
|
10262
|
-
});
|
|
10263
|
-
builderBrief = parseArchitectOutput(architectResult.output);
|
|
10264
|
-
architectTokens = architectResult.tokens;
|
|
10265
|
-
synthesisCount = 0;
|
|
10266
|
-
await onProgress?.({
|
|
10267
|
-
type: 'phase_complete',
|
|
10268
|
-
path: node.ownerPath,
|
|
10269
|
-
phase: 'architect',
|
|
10270
|
-
tokens: architectTokens,
|
|
10271
|
-
durationMs: Date.now() - phaseStart,
|
|
10272
|
-
});
|
|
10273
|
-
}
|
|
10274
|
-
catch (err) {
|
|
10275
|
-
stepError = toMetaError('architect', err);
|
|
10276
|
-
if (!currentMeta._builder) {
|
|
10277
|
-
// No cached builder — cycle fails
|
|
10278
|
-
await finalizeCycle({
|
|
10279
|
-
...baseFinalizeOptions,
|
|
10280
|
-
builder: '',
|
|
10281
|
-
builderOutput: null,
|
|
10282
|
-
feedback: null,
|
|
10283
|
-
synthesisCount,
|
|
10284
|
-
error: stepError,
|
|
10285
|
-
architectTokens,
|
|
10286
|
-
});
|
|
10287
|
-
return {
|
|
10288
|
-
synthesized: true,
|
|
10289
|
-
metaPath: node.metaPath,
|
|
10290
|
-
error: stepError,
|
|
10291
|
-
};
|
|
10292
|
-
}
|
|
10293
|
-
// Has cached builder — continue with existing
|
|
10294
|
-
}
|
|
10295
|
-
}
|
|
10296
|
-
// Step 9: Builder
|
|
10297
|
-
const metaForBuilder = { ...currentMeta, _builder: builderBrief };
|
|
10298
|
-
let builderOutput;
|
|
10299
|
-
try {
|
|
10300
|
-
await onProgress?.({
|
|
10301
|
-
type: 'phase_start',
|
|
10302
|
-
path: node.ownerPath,
|
|
10303
|
-
phase: 'builder',
|
|
10304
|
-
});
|
|
10305
|
-
const builderStart = Date.now();
|
|
10306
|
-
const builderTask = buildBuilderTask(ctx, metaForBuilder, config);
|
|
10307
|
-
const builderResult = await executor.spawn(builderTask, {
|
|
10308
|
-
thinking: config.thinking,
|
|
10309
|
-
timeout: config.builderTimeout,
|
|
10310
|
-
label: 'meta-builder',
|
|
10311
|
-
});
|
|
10312
|
-
builderOutput = parseBuilderOutput(builderResult.output);
|
|
10313
|
-
builderTokens = builderResult.tokens;
|
|
10314
|
-
synthesisCount++;
|
|
10315
|
-
await onProgress?.({
|
|
10316
|
-
type: 'phase_complete',
|
|
10317
|
-
path: node.ownerPath,
|
|
10318
|
-
phase: 'builder',
|
|
10319
|
-
tokens: builderTokens,
|
|
10320
|
-
durationMs: Date.now() - builderStart,
|
|
10321
|
-
});
|
|
10322
|
-
}
|
|
10323
|
-
catch (err) {
|
|
10324
|
-
if (err instanceof SpawnTimeoutError) {
|
|
10325
|
-
const recovered = await attemptTimeoutRecovery({
|
|
10326
|
-
err,
|
|
10327
|
-
currentMeta,
|
|
10328
|
-
metaPath: node.metaPath,
|
|
10329
|
-
config,
|
|
10330
|
-
builderBrief,
|
|
10331
|
-
structureHash: newStructureHash,
|
|
10332
|
-
synthesisCount,
|
|
10333
|
-
});
|
|
10334
|
-
if (recovered)
|
|
10335
|
-
return recovered;
|
|
10336
|
-
}
|
|
10337
|
-
stepError = toMetaError('builder', err);
|
|
10338
|
-
await finalizeCycle({
|
|
10339
|
-
...baseFinalizeOptions,
|
|
10340
|
-
builder: builderBrief,
|
|
10341
|
-
builderOutput: null,
|
|
10342
|
-
feedback: null,
|
|
10343
|
-
synthesisCount,
|
|
10344
|
-
error: stepError,
|
|
10345
|
-
});
|
|
10346
|
-
return { synthesized: true, metaPath: node.metaPath, error: stepError };
|
|
10347
|
-
}
|
|
10348
|
-
// Step 10: Critic
|
|
10349
|
-
const metaForCritic = {
|
|
10350
|
-
...currentMeta,
|
|
10351
|
-
_content: builderOutput.content,
|
|
10352
|
-
};
|
|
10353
|
-
let feedback = null;
|
|
10354
|
-
try {
|
|
10355
|
-
await onProgress?.({
|
|
10356
|
-
type: 'phase_start',
|
|
10357
|
-
path: node.ownerPath,
|
|
10358
|
-
phase: 'critic',
|
|
10359
|
-
});
|
|
10360
|
-
const criticStart = Date.now();
|
|
10361
|
-
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
10362
|
-
const criticResult = await executor.spawn(criticTask, {
|
|
10363
|
-
thinking: config.thinking,
|
|
10364
|
-
timeout: config.criticTimeout,
|
|
10365
|
-
label: 'meta-critic',
|
|
10366
|
-
});
|
|
10367
|
-
feedback = parseCriticOutput(criticResult.output);
|
|
10368
|
-
criticTokens = criticResult.tokens;
|
|
10369
|
-
stepError = null; // Clear any architect error on full success
|
|
10370
|
-
await onProgress?.({
|
|
10371
|
-
type: 'phase_complete',
|
|
10372
|
-
path: node.ownerPath,
|
|
10373
|
-
phase: 'critic',
|
|
10374
|
-
tokens: criticTokens,
|
|
10375
|
-
durationMs: Date.now() - criticStart,
|
|
10376
|
-
});
|
|
10377
|
-
}
|
|
10378
|
-
catch (err) {
|
|
10379
|
-
stepError = stepError ?? toMetaError('critic', err);
|
|
10380
|
-
}
|
|
10381
|
-
// Steps 11-12: Merge, archive, prune
|
|
10382
|
-
await finalizeCycle({
|
|
10383
|
-
...baseFinalizeOptions,
|
|
10384
|
-
builder: builderBrief,
|
|
10385
|
-
builderOutput,
|
|
10386
|
-
feedback,
|
|
10387
|
-
synthesisCount,
|
|
10388
|
-
error: stepError,
|
|
10389
|
-
architectTokens,
|
|
10390
|
-
builderTokens,
|
|
10391
|
-
criticTokens,
|
|
10392
|
-
state: builderOutput.state,
|
|
10393
|
-
});
|
|
10394
|
-
return {
|
|
10395
|
-
synthesized: true,
|
|
10396
|
-
metaPath: node.metaPath,
|
|
10397
|
-
error: stepError ?? undefined,
|
|
10398
|
-
};
|
|
10399
|
-
}
|
|
10400
|
-
|
|
10401
|
-
/**
|
|
10402
|
-
* Main orchestration entry point — discovery, scheduling, candidate selection.
|
|
10403
|
-
*
|
|
10404
|
-
* @module orchestrator/orchestrate
|
|
10405
|
-
*/
|
|
10406
|
-
async function orchestrateOnce(config, executor, watcher, targetPath, onProgress, logger) {
|
|
10407
|
-
// When targetPath is provided, skip the expensive full discovery scan.
|
|
10408
|
-
// Build a minimal node from the filesystem instead.
|
|
10409
|
-
if (targetPath) {
|
|
10410
|
-
const normalizedTarget = normalizePath(targetPath);
|
|
10411
|
-
const targetMetaJson = join(normalizedTarget, 'meta.json');
|
|
10412
|
-
if (!existsSync(targetMetaJson))
|
|
10413
|
-
return { synthesized: false };
|
|
10414
|
-
const node = await buildMinimalNode(normalizedTarget, watcher);
|
|
10415
|
-
if (!acquireLock(node.metaPath))
|
|
10416
|
-
return { synthesized: false };
|
|
10417
|
-
try {
|
|
10418
|
-
const currentMeta = await readMetaJson(normalizedTarget);
|
|
10419
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
10420
|
-
}
|
|
10421
|
-
finally {
|
|
10422
|
-
releaseLock(node.metaPath);
|
|
10423
|
-
}
|
|
10424
|
-
}
|
|
10425
|
-
// Full discovery path (scheduler-driven, no specific target)
|
|
10426
|
-
// Step 1: Discover via watcher walk
|
|
10427
|
-
const discoveryStart = Date.now();
|
|
10428
|
-
const metaPaths = await discoverMetas(watcher);
|
|
10429
|
-
logger?.debug({ paths: metaPaths.length, durationMs: Date.now() - discoveryStart }, 'discovery complete');
|
|
10430
|
-
if (metaPaths.length === 0)
|
|
10431
|
-
return { synthesized: false };
|
|
10432
|
-
// Read meta.json for each discovered meta
|
|
10433
|
-
const metas = new Map();
|
|
10434
|
-
for (const mp of metaPaths) {
|
|
10435
|
-
try {
|
|
10436
|
-
metas.set(normalizePath(mp), await readMetaJson(mp));
|
|
10437
|
-
}
|
|
10438
|
-
catch {
|
|
10439
|
-
// Skip metas with unreadable meta.json
|
|
10440
|
-
continue;
|
|
10441
|
-
}
|
|
10442
|
-
}
|
|
10443
|
-
// Only build tree from paths with readable meta.json (excludes orphaned/deleted entries)
|
|
10444
|
-
const validPaths = metaPaths.filter((mp) => metas.has(normalizePath(mp)));
|
|
10445
|
-
if (validPaths.length === 0)
|
|
10446
|
-
return { synthesized: false };
|
|
10447
|
-
const tree = buildOwnershipTree(validPaths);
|
|
10448
|
-
// Steps 3-4: Staleness check + candidate selection
|
|
10449
|
-
const candidates = [];
|
|
10450
|
-
for (const treeNode of tree.nodes.values()) {
|
|
10451
|
-
const meta = metas.get(treeNode.metaPath);
|
|
10452
|
-
if (!meta)
|
|
10453
|
-
continue;
|
|
10454
|
-
const staleness = actualStaleness(meta);
|
|
10455
|
-
if (staleness > 0) {
|
|
10456
|
-
candidates.push({ node: treeNode, meta, actualStaleness: staleness });
|
|
10457
|
-
}
|
|
10458
|
-
}
|
|
10459
|
-
const weighted = computeEffectiveStaleness(candidates, config.depthWeight);
|
|
10460
|
-
// Sort by effective staleness descending
|
|
10461
|
-
const ranked = [...weighted].sort((a, b) => b.effectiveStaleness - a.effectiveStaleness);
|
|
10462
|
-
if (ranked.length === 0)
|
|
10463
|
-
return { synthesized: false };
|
|
10464
|
-
// Find the first candidate with actual changes (if skipUnchanged)
|
|
10465
|
-
let winner = null;
|
|
10466
|
-
for (const candidate of ranked) {
|
|
10467
|
-
if (!acquireLock(candidate.node.metaPath))
|
|
10468
|
-
continue;
|
|
10469
|
-
const verifiedStale = await isStale(getScopePrefix(candidate.node), candidate.meta, watcher);
|
|
10470
|
-
if (!verifiedStale && candidate.meta._generatedAt) {
|
|
10471
|
-
// Bump _generatedAt so it doesn't win next cycle
|
|
10472
|
-
const freshMeta = await readMetaJson(candidate.node.metaPath);
|
|
10473
|
-
freshMeta._generatedAt = new Date().toISOString();
|
|
10474
|
-
await writeFile(join(candidate.node.metaPath, 'meta.json'), JSON.stringify(freshMeta, null, 2));
|
|
10475
|
-
releaseLock(candidate.node.metaPath);
|
|
10476
|
-
if (config.skipUnchanged)
|
|
10477
|
-
continue;
|
|
10478
|
-
return { synthesized: false };
|
|
10479
|
-
}
|
|
10480
|
-
winner = candidate;
|
|
10481
|
-
break;
|
|
10482
|
-
}
|
|
10483
|
-
if (!winner)
|
|
10484
|
-
return { synthesized: false };
|
|
10485
|
-
const node = winner.node;
|
|
10486
|
-
try {
|
|
10487
|
-
const currentMeta = await readMetaJson(node.metaPath);
|
|
10488
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
10489
|
-
}
|
|
10490
|
-
finally {
|
|
10491
|
-
// Step 13: Release lock
|
|
10492
|
-
releaseLock(node.metaPath);
|
|
10493
|
-
}
|
|
10494
|
-
}
|
|
10495
|
-
/**
|
|
10496
|
-
* Run a single synthesis cycle.
|
|
10497
|
-
*
|
|
10498
|
-
* Selects the stalest candidate (or a specific target) and runs the
|
|
10499
|
-
* full architect/builder/critic pipeline.
|
|
10500
|
-
*
|
|
10501
|
-
* @param config - Validated synthesis config.
|
|
10502
|
-
* @param executor - Pluggable LLM executor.
|
|
10503
|
-
* @param watcher - Watcher HTTP client.
|
|
10504
|
-
* @param targetPath - Optional: specific meta/owner path to synthesize instead of stalest candidate.
|
|
10505
|
-
* @returns Array with a single result.
|
|
10506
|
-
*/
|
|
10507
|
-
async function orchestrate(config, executor, watcher, targetPath, onProgress, logger) {
|
|
10508
|
-
const result = await orchestrateOnce(config, executor, watcher, targetPath, onProgress, logger);
|
|
10509
|
-
return [result];
|
|
10510
|
-
}
|
|
10511
|
-
|
|
10512
|
-
/**
|
|
10513
|
-
* Pure phase-state transition functions.
|
|
10514
|
-
*
|
|
10515
|
-
* Implements every row of the §8 "Transitions and invalidation cascade" table.
|
|
10516
|
-
* No I/O — pure functions over PhaseState and documented inputs.
|
|
10517
|
-
*
|
|
10518
|
-
* @module phaseState/phaseTransitions
|
|
10519
|
-
*/
|
|
10520
|
-
/**
|
|
10521
|
-
* Create a fresh (fully-complete) phase state.
|
|
10522
|
-
*/
|
|
10523
|
-
function freshPhaseState() {
|
|
10524
|
-
return { architect: 'fresh', builder: 'fresh', critic: 'fresh' };
|
|
10525
|
-
}
|
|
10526
|
-
/**
|
|
10527
|
-
* Create a phase state for a never-synthesized meta (all pending from architect).
|
|
10528
|
-
*/
|
|
10529
|
-
function initialPhaseState() {
|
|
10530
|
-
return { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
10531
|
-
}
|
|
10532
|
-
/**
|
|
10533
|
-
* Enforce the per-meta invariant: at most one phase is pending or running,
|
|
10534
|
-
* and it is the first non-fresh phase in pipeline order.
|
|
10535
|
-
*
|
|
10536
|
-
* Stale phases that become the first non-fresh phase are promoted to pending.
|
|
10537
|
-
*/
|
|
10538
|
-
function enforceInvariant(state) {
|
|
10539
|
-
const result = { ...state };
|
|
10540
|
-
let foundNonFresh = false;
|
|
10541
|
-
for (const phase of ['architect', 'builder', 'critic']) {
|
|
10542
|
-
const s = result[phase];
|
|
10543
|
-
if (s === 'fresh')
|
|
10544
|
-
continue;
|
|
10545
|
-
if (!foundNonFresh) {
|
|
10546
|
-
foundNonFresh = true;
|
|
10547
|
-
// First non-fresh: if stale, promote to pending
|
|
10548
|
-
if (s === 'stale') {
|
|
10549
|
-
result[phase] = 'pending';
|
|
10550
|
-
}
|
|
10551
|
-
// pending, running, failed stay as-is
|
|
10552
|
-
}
|
|
10553
|
-
else {
|
|
10554
|
-
// Subsequent non-fresh: must not be pending or running
|
|
10555
|
-
if (s === 'pending') {
|
|
10556
|
-
result[phase] = 'stale';
|
|
10557
|
-
}
|
|
10558
|
-
// running in non-first position would be a bug, but don't mask it
|
|
10559
|
-
}
|
|
10560
|
-
}
|
|
10561
|
-
return result;
|
|
10562
|
-
}
|
|
10563
|
-
// ── Phase success transitions ──────────────────────────────────────────
|
|
10564
|
-
/**
|
|
10565
|
-
* Architect completes successfully.
|
|
10566
|
-
* architect → fresh; builder → pending; critic → stale.
|
|
10567
|
-
*/
|
|
10568
|
-
function architectSuccess(state) {
|
|
10569
|
-
return enforceInvariant({
|
|
10570
|
-
architect: 'fresh',
|
|
10571
|
-
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
10572
|
-
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
10573
|
-
});
|
|
10574
|
-
}
|
|
10575
|
-
/**
|
|
10576
|
-
* Builder completes successfully.
|
|
10577
|
-
* builder → fresh; critic → pending.
|
|
10578
|
-
*/
|
|
10579
|
-
function builderSuccess(state) {
|
|
10580
|
-
return enforceInvariant({
|
|
10581
|
-
...state,
|
|
10582
|
-
builder: 'fresh',
|
|
10583
|
-
critic: state.critic === 'failed' ? 'failed' : 'pending',
|
|
10584
|
-
});
|
|
10585
|
-
}
|
|
10586
|
-
/**
|
|
10587
|
-
* Critic completes successfully.
|
|
10588
|
-
* critic → fresh. Meta becomes fully fresh.
|
|
10589
|
-
*/
|
|
10590
|
-
function criticSuccess(state) {
|
|
10591
|
-
return enforceInvariant({
|
|
10592
|
-
...state,
|
|
10593
|
-
critic: 'fresh',
|
|
10594
|
-
});
|
|
10595
|
-
}
|
|
10596
|
-
// ── Failure transition ─────────────────────────────────────────────────
|
|
10597
|
-
/**
|
|
10598
|
-
* A phase fails (error, timeout, or abort).
|
|
10599
|
-
* Target phase → failed; upstream and downstream unchanged.
|
|
10600
|
-
*/
|
|
10601
|
-
function phaseFailed(state, phase) {
|
|
10602
|
-
return enforceInvariant({
|
|
10603
|
-
...state,
|
|
10604
|
-
[phase]: 'failed',
|
|
10605
|
-
});
|
|
10606
|
-
}
|
|
10607
|
-
// ── Surgical retry ─────────────────────────────────────────────────────
|
|
10608
|
-
/**
|
|
10609
|
-
* Retry a failed phase: failed → pending.
|
|
10610
|
-
* Only valid when the phase is currently failed.
|
|
10611
|
-
*/
|
|
10612
|
-
function retryPhase(state, phase) {
|
|
10613
|
-
if (state[phase] !== 'failed')
|
|
10614
|
-
return state;
|
|
10615
|
-
return enforceInvariant({
|
|
10616
|
-
...state,
|
|
10617
|
-
[phase]: 'pending',
|
|
10618
|
-
});
|
|
10619
|
-
}
|
|
10620
|
-
/**
|
|
10621
|
-
* Retry all failed phases: each failed phase → pending.
|
|
10622
|
-
* Used by scheduler ticks and queue reads to auto-promote failed phases.
|
|
10623
|
-
*/
|
|
10624
|
-
function retryAllFailed(state) {
|
|
10625
|
-
let result = state;
|
|
10626
|
-
for (const phase of ['architect', 'builder', 'critic']) {
|
|
10627
|
-
if (result[phase] === 'failed') {
|
|
10628
|
-
result = retryPhase(result, phase);
|
|
10629
|
-
}
|
|
10630
|
-
}
|
|
10631
|
-
return result;
|
|
10632
|
-
}
|
|
10633
|
-
// ── Running transition ─────────────────────────────────────────────────
|
|
10634
|
-
/**
|
|
10635
|
-
* Mark a phase as running (scheduler picks it).
|
|
10636
|
-
*/
|
|
10637
|
-
function phaseRunning(state, phase) {
|
|
10638
|
-
return {
|
|
10639
|
-
...state,
|
|
10640
|
-
[phase]: 'running',
|
|
10641
|
-
};
|
|
10642
|
-
}
|
|
10643
|
-
// ── Query helpers ──────────────────────────────────────────────────────
|
|
10644
|
-
/**
|
|
10645
|
-
* Get the owed phase: first non-fresh phase in pipeline order, or null.
|
|
10646
|
-
*/
|
|
10647
|
-
function getOwedPhase(state) {
|
|
10648
|
-
for (const phase of ['architect', 'builder', 'critic']) {
|
|
10649
|
-
if (state[phase] !== 'fresh')
|
|
10650
|
-
return phase;
|
|
10651
|
-
}
|
|
10652
|
-
return null;
|
|
10653
|
-
}
|
|
10654
|
-
/**
|
|
10655
|
-
* Check if a meta is fully fresh (all phases fresh).
|
|
10656
|
-
*/
|
|
10657
|
-
function isFullyFresh(state) {
|
|
10658
|
-
return (state.architect === 'fresh' &&
|
|
10659
|
-
state.builder === 'fresh' &&
|
|
10660
|
-
state.critic === 'fresh');
|
|
10661
|
-
}
|
|
10662
|
-
/**
|
|
10663
|
-
* Get the scheduler priority band for a meta's owed phase.
|
|
10664
|
-
* 1 = critic (highest), 2 = builder, 3 = architect, null = fully fresh.
|
|
10665
|
-
*/
|
|
10666
|
-
function getPriorityBand(state) {
|
|
10667
|
-
const owed = getOwedPhase(state);
|
|
10668
|
-
if (!owed)
|
|
10669
|
-
return null;
|
|
10670
|
-
if (owed === 'critic')
|
|
10671
|
-
return 1;
|
|
10672
|
-
if (owed === 'builder')
|
|
10673
|
-
return 2;
|
|
10674
|
-
return 3;
|
|
10675
|
-
}
|
|
10676
|
-
|
|
10677
|
-
/**
|
|
10678
|
-
* Backward-compatible derivation of _phaseState from existing meta fields.
|
|
10679
|
-
*
|
|
10680
|
-
* When a meta is loaded from disk without _phaseState, this reconstructs
|
|
10681
|
-
* the phase state from _content, _builder, _state, _error.step, and
|
|
10682
|
-
* the architect-invalidating inputs.
|
|
10683
|
-
*
|
|
10684
|
-
* @module phaseState/derivePhaseState
|
|
10685
|
-
*/
|
|
10686
|
-
/**
|
|
10687
|
-
* Derive _phaseState from existing meta fields.
|
|
10688
|
-
*
|
|
10689
|
-
* If the meta already has _phaseState, returns it as-is.
|
|
10690
|
-
*
|
|
10691
|
-
* Otherwise, reconstructs from available fields:
|
|
10692
|
-
* - Never-synthesized meta (no _content, no _builder): all phases start pending/stale.
|
|
10693
|
-
* - Errored meta: the failed phase is mapped from _error.step.
|
|
10694
|
-
* - Mid-cycle meta with cached _builder but no _content: builder pending.
|
|
10695
|
-
* - Fully-fresh meta: all phases fresh.
|
|
10696
|
-
* - Meta with stale architect inputs: architect pending, downstream stale.
|
|
10697
|
-
*
|
|
10698
|
-
* @param meta - The meta.json content.
|
|
10699
|
-
* @param inputs - Optional derivation inputs. If not provided, a simpler
|
|
10700
|
-
* heuristic is used (no architect invalidation check).
|
|
10701
|
-
* @returns The derived PhaseState.
|
|
10702
|
-
*/
|
|
10703
|
-
function derivePhaseState(meta, inputs) {
|
|
10704
|
-
// Already has _phaseState — use it
|
|
10705
|
-
if (meta._phaseState)
|
|
10706
|
-
return meta._phaseState;
|
|
10707
|
-
// Check for errors first — _error.step maps directly to failed phase
|
|
10708
|
-
if (meta._error) {
|
|
10709
|
-
const failedPhase = meta._error.step;
|
|
10710
|
-
const state = freshPhaseState();
|
|
10711
|
-
state[failedPhase] = 'failed';
|
|
10712
|
-
// If architect failed and no _builder, downstream is stale
|
|
10713
|
-
if (failedPhase === 'architect') {
|
|
10714
|
-
if (!meta._builder) {
|
|
10715
|
-
state.builder = 'stale';
|
|
10716
|
-
state.critic = 'stale';
|
|
10717
|
-
}
|
|
10718
|
-
}
|
|
10719
|
-
// If builder failed, critic is stale
|
|
10720
|
-
if (failedPhase === 'builder') {
|
|
10721
|
-
state.critic = 'stale';
|
|
10722
|
-
}
|
|
10723
|
-
return state;
|
|
10724
|
-
}
|
|
10725
|
-
// Never synthesized: no _content AND no _builder (and no error)
|
|
10726
|
-
if (!meta._content && !meta._builder) {
|
|
10727
|
-
return initialPhaseState();
|
|
10728
|
-
}
|
|
10729
|
-
// Check architect invalidation (when inputs are provided)
|
|
10730
|
-
if (inputs) {
|
|
10731
|
-
const architectInvalidated = inputs.structureChanged ||
|
|
10732
|
-
inputs.steerChanged ||
|
|
10733
|
-
inputs.architectChanged ||
|
|
10734
|
-
inputs.crossRefsChanged ||
|
|
10735
|
-
(meta._synthesisCount ?? 0) >= inputs.architectEvery;
|
|
10736
|
-
if (architectInvalidated) {
|
|
10737
|
-
return {
|
|
10738
|
-
architect: 'pending',
|
|
10739
|
-
builder: 'stale',
|
|
10740
|
-
critic: 'stale',
|
|
10741
|
-
};
|
|
10443
|
+
}
|
|
10742
10444
|
}
|
|
10445
|
+
return { content, fields, ...(state !== undefined ? { state } : {}) };
|
|
10743
10446
|
}
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
return {
|
|
10747
|
-
architect: 'fresh',
|
|
10748
|
-
builder: 'pending',
|
|
10749
|
-
critic: 'stale',
|
|
10750
|
-
};
|
|
10751
|
-
}
|
|
10752
|
-
// Has _content but no _feedback: critic is pending
|
|
10753
|
-
if (meta._content && !meta._feedback) {
|
|
10754
|
-
return {
|
|
10755
|
-
architect: 'fresh',
|
|
10756
|
-
builder: 'fresh',
|
|
10757
|
-
critic: 'pending',
|
|
10758
|
-
};
|
|
10447
|
+
catch {
|
|
10448
|
+
return null;
|
|
10759
10449
|
}
|
|
10760
|
-
// Default: fully fresh
|
|
10761
|
-
return freshPhaseState();
|
|
10762
|
-
}
|
|
10763
|
-
|
|
10764
|
-
/**
|
|
10765
|
-
* Corpus-wide phase scheduler.
|
|
10766
|
-
*
|
|
10767
|
-
* Selects the highest-priority ready phase across all metas.
|
|
10768
|
-
* Priority: critic (band 1) \> builder (band 2) \> architect (band 3).
|
|
10769
|
-
* Tiebreak within band: weighted staleness (§3.9).
|
|
10770
|
-
*
|
|
10771
|
-
* @module phaseState/phaseScheduler
|
|
10772
|
-
*/
|
|
10773
|
-
/**
|
|
10774
|
-
* Build phase candidates from listMetas entries.
|
|
10775
|
-
*
|
|
10776
|
-
* Derives phase state and auto-retries failed phases for each entry.
|
|
10777
|
-
* Used by orchestratePhase, queue route, and status route.
|
|
10778
|
-
*/
|
|
10779
|
-
function buildPhaseCandidates(entries) {
|
|
10780
|
-
return entries.map((entry) => ({
|
|
10781
|
-
node: entry.node,
|
|
10782
|
-
meta: entry.meta,
|
|
10783
|
-
phaseState: retryAllFailed(derivePhaseState(entry.meta)),
|
|
10784
|
-
actualStaleness: entry.stalenessSeconds,
|
|
10785
|
-
locked: entry.locked,
|
|
10786
|
-
disabled: entry.disabled,
|
|
10787
|
-
}));
|
|
10788
|
-
}
|
|
10789
|
-
/**
|
|
10790
|
-
* Rank all eligible phase candidates by priority.
|
|
10791
|
-
*
|
|
10792
|
-
* Filters to pending phases, computes effective staleness, and sorts by
|
|
10793
|
-
* band (ascending: critic first) then effective staleness (descending).
|
|
10794
|
-
*
|
|
10795
|
-
* Used by selectPhaseCandidate (returns first) and the queue route (returns all).
|
|
10796
|
-
*/
|
|
10797
|
-
function rankPhaseCandidates(metas, depthWeight) {
|
|
10798
|
-
// Filter to metas with a pending (scheduler-eligible) phase
|
|
10799
|
-
const eligible = metas.filter((m) => {
|
|
10800
|
-
if (m.locked)
|
|
10801
|
-
return false;
|
|
10802
|
-
if (m.disabled && !m.isOverride)
|
|
10803
|
-
return false;
|
|
10804
|
-
const owed = getOwedPhase(m.phaseState);
|
|
10805
|
-
if (!owed)
|
|
10806
|
-
return false;
|
|
10807
|
-
return m.phaseState[owed] === 'pending';
|
|
10808
|
-
});
|
|
10809
|
-
if (eligible.length === 0)
|
|
10810
|
-
return [];
|
|
10811
|
-
// Compute effective staleness for tiebreaking
|
|
10812
|
-
const withStaleness = computeEffectiveStaleness(eligible.map((m) => ({
|
|
10813
|
-
node: m.node,
|
|
10814
|
-
meta: m.meta,
|
|
10815
|
-
actualStaleness: m.actualStaleness,
|
|
10816
|
-
})), depthWeight);
|
|
10817
|
-
// Build candidates with band info
|
|
10818
|
-
const candidates = withStaleness.map((ws, i) => {
|
|
10819
|
-
const m = eligible[i];
|
|
10820
|
-
const owedPhase = getOwedPhase(m.phaseState);
|
|
10821
|
-
return {
|
|
10822
|
-
node: ws.node,
|
|
10823
|
-
meta: ws.meta,
|
|
10824
|
-
phaseState: m.phaseState,
|
|
10825
|
-
owedPhase,
|
|
10826
|
-
band: getPriorityBand(m.phaseState),
|
|
10827
|
-
actualStaleness: ws.actualStaleness,
|
|
10828
|
-
effectiveStaleness: ws.effectiveStaleness,
|
|
10829
|
-
};
|
|
10830
|
-
});
|
|
10831
|
-
// Sort by band (ascending = critic first) then effective staleness (descending)
|
|
10832
|
-
candidates.sort((a, b) => {
|
|
10833
|
-
if (a.band !== b.band)
|
|
10834
|
-
return a.band - b.band;
|
|
10835
|
-
return b.effectiveStaleness - a.effectiveStaleness;
|
|
10836
|
-
});
|
|
10837
|
-
return candidates;
|
|
10838
10450
|
}
|
|
10839
10451
|
/**
|
|
10840
|
-
*
|
|
10452
|
+
* Parse critic output. The critic returns evaluation text.
|
|
10841
10453
|
*
|
|
10842
|
-
* @param
|
|
10843
|
-
* @
|
|
10844
|
-
* @returns The winning candidate, or null if no phase is ready.
|
|
10454
|
+
* @param output - Raw subprocess output.
|
|
10455
|
+
* @returns The feedback string.
|
|
10845
10456
|
*/
|
|
10846
|
-
function
|
|
10847
|
-
return
|
|
10457
|
+
function parseCriticOutput(output) {
|
|
10458
|
+
return output.trim();
|
|
10848
10459
|
}
|
|
10849
10460
|
|
|
10850
10461
|
/**
|
|
@@ -11105,7 +10716,7 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
|
|
|
11105
10716
|
if (metaResult.entries.length === 0)
|
|
11106
10717
|
return { executed: false };
|
|
11107
10718
|
// Build candidates with phase state (including invalidation + auto-retry)
|
|
11108
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
10719
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
11109
10720
|
// Select best phase candidate
|
|
11110
10721
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
11111
10722
|
if (!winner) {
|
|
@@ -11780,46 +11391,6 @@ function buildMetaRules(config) {
|
|
|
11780
11391
|
},
|
|
11781
11392
|
renderAs: 'md',
|
|
11782
11393
|
},
|
|
11783
|
-
{
|
|
11784
|
-
name: 'meta-config',
|
|
11785
|
-
description: 'jeeves-meta configuration file',
|
|
11786
|
-
match: {
|
|
11787
|
-
properties: {
|
|
11788
|
-
file: {
|
|
11789
|
-
properties: {
|
|
11790
|
-
path: {
|
|
11791
|
-
type: 'string',
|
|
11792
|
-
glob: '**/jeeves-meta{.config.json,/config.json}',
|
|
11793
|
-
},
|
|
11794
|
-
},
|
|
11795
|
-
},
|
|
11796
|
-
},
|
|
11797
|
-
},
|
|
11798
|
-
schema: ['base', { properties: { domains: { set: ['meta-config'] } } }],
|
|
11799
|
-
render: {
|
|
11800
|
-
frontmatter: [
|
|
11801
|
-
'watcherUrl',
|
|
11802
|
-
'gatewayUrl',
|
|
11803
|
-
'architectEvery',
|
|
11804
|
-
'depthWeight',
|
|
11805
|
-
'maxArchive',
|
|
11806
|
-
'maxLines',
|
|
11807
|
-
],
|
|
11808
|
-
body: [
|
|
11809
|
-
{
|
|
11810
|
-
path: 'json.defaultArchitect',
|
|
11811
|
-
heading: 2,
|
|
11812
|
-
label: 'Default Architect Prompt',
|
|
11813
|
-
},
|
|
11814
|
-
{
|
|
11815
|
-
path: 'json.defaultCritic',
|
|
11816
|
-
heading: 2,
|
|
11817
|
-
label: 'Default Critic Prompt',
|
|
11818
|
-
},
|
|
11819
|
-
],
|
|
11820
|
-
},
|
|
11821
|
-
renderAs: 'md',
|
|
11822
|
-
},
|
|
11823
11394
|
];
|
|
11824
11395
|
}
|
|
11825
11396
|
/**
|
|
@@ -12063,13 +11634,15 @@ class Scheduler {
|
|
|
12063
11634
|
queue;
|
|
12064
11635
|
logger;
|
|
12065
11636
|
watcher;
|
|
11637
|
+
cache;
|
|
12066
11638
|
registrar = null;
|
|
12067
11639
|
currentExpression;
|
|
12068
|
-
constructor(config, queue, logger, watcher) {
|
|
11640
|
+
constructor(config, queue, logger, watcher, cache) {
|
|
12069
11641
|
this.config = config;
|
|
12070
11642
|
this.queue = queue;
|
|
12071
11643
|
this.logger = logger;
|
|
12072
11644
|
this.watcher = watcher;
|
|
11645
|
+
this.cache = cache;
|
|
12073
11646
|
this.currentExpression = config.schedule;
|
|
12074
11647
|
}
|
|
12075
11648
|
/** Set the rule registrar for watcher restart detection. */
|
|
@@ -12186,8 +11759,8 @@ class Scheduler {
|
|
|
12186
11759
|
*/
|
|
12187
11760
|
async discoverNextPhase() {
|
|
12188
11761
|
try {
|
|
12189
|
-
const result = await
|
|
12190
|
-
const candidates = buildPhaseCandidates(result.entries);
|
|
11762
|
+
const result = await this.cache.get(this.config, this.watcher);
|
|
11763
|
+
const candidates = buildPhaseCandidates(result.entries, this.config.architectEvery);
|
|
12191
11764
|
const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
|
|
12192
11765
|
if (!winner)
|
|
12193
11766
|
return null;
|
|
@@ -12558,7 +12131,7 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12558
12131
|
const metaDir = resolveMetaDir(targetPath);
|
|
12559
12132
|
let meta;
|
|
12560
12133
|
try {
|
|
12561
|
-
meta =
|
|
12134
|
+
meta = await readMetaJson(metaDir);
|
|
12562
12135
|
}
|
|
12563
12136
|
catch {
|
|
12564
12137
|
return reply.status(404).send({
|
|
@@ -12612,11 +12185,11 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12612
12185
|
*/
|
|
12613
12186
|
function registerPreviewRoute(app, deps) {
|
|
12614
12187
|
app.get('/preview', async (request, reply) => {
|
|
12615
|
-
const { config, watcher } = deps;
|
|
12188
|
+
const { config, watcher, cache } = deps;
|
|
12616
12189
|
const query = request.query;
|
|
12617
12190
|
let result;
|
|
12618
12191
|
try {
|
|
12619
|
-
result = await
|
|
12192
|
+
result = await cache.get(config, watcher);
|
|
12620
12193
|
}
|
|
12621
12194
|
catch {
|
|
12622
12195
|
return reply.status(503).send({
|
|
@@ -12636,40 +12209,24 @@ function registerPreviewRoute(app, deps) {
|
|
|
12636
12209
|
}
|
|
12637
12210
|
}
|
|
12638
12211
|
else {
|
|
12639
|
-
// Select
|
|
12640
|
-
const
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
node: e.node,
|
|
12644
|
-
meta: e.meta,
|
|
12645
|
-
actualStaleness: e.stalenessSeconds,
|
|
12646
|
-
}));
|
|
12647
|
-
const stalestPath = discoverStalestPath(stale, config.depthWeight);
|
|
12648
|
-
if (!stalestPath) {
|
|
12212
|
+
// Select best phase candidate
|
|
12213
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
12214
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12215
|
+
if (!winner) {
|
|
12649
12216
|
return { message: 'No stale metas found. Nothing to synthesize.' };
|
|
12650
12217
|
}
|
|
12651
|
-
targetNode = findNode(result.tree,
|
|
12218
|
+
targetNode = findNode(result.tree, winner.node.metaPath);
|
|
12652
12219
|
}
|
|
12653
12220
|
const meta = await readMetaJson(targetNode.metaPath);
|
|
12654
12221
|
// Scope files
|
|
12655
12222
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
12656
|
-
|
|
12223
|
+
// Compute invalidation inputs (DRY: reuse phaseState/invalidate logic)
|
|
12224
|
+
const invalidation = await computeInvalidation(meta, scopeFiles, config, targetNode);
|
|
12225
|
+
const { architectInvalidators, stalenessInputs } = invalidation;
|
|
12226
|
+
const { structureHash } = invalidation;
|
|
12657
12227
|
const structureChanged = structureHash !== meta._structureHash;
|
|
12658
|
-
const
|
|
12659
|
-
const
|
|
12660
|
-
// _architect change detection
|
|
12661
|
-
const architectChanged = latestArchive
|
|
12662
|
-
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
12663
|
-
: Boolean(meta._architect);
|
|
12664
|
-
// _crossRefs declaration change detection
|
|
12665
|
-
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
12666
|
-
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
12667
|
-
.slice()
|
|
12668
|
-
.sort()
|
|
12669
|
-
.join(',');
|
|
12670
|
-
const crossRefsDeclChanged = latestArchive
|
|
12671
|
-
? currentRefs !== archiveRefs
|
|
12672
|
-
: currentRefs.length > 0;
|
|
12228
|
+
const { steerChanged } = invalidation;
|
|
12229
|
+
const { architectChanged, crossRefsDeclChanged } = stalenessInputs;
|
|
12673
12230
|
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
12674
12231
|
// Delta files
|
|
12675
12232
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
@@ -12694,30 +12251,6 @@ function registerPreviewRoute(app, deps) {
|
|
|
12694
12251
|
});
|
|
12695
12252
|
const owedPhase = getOwedPhase(phaseState);
|
|
12696
12253
|
const priorityBand = getPriorityBand(phaseState);
|
|
12697
|
-
// Architect invalidators
|
|
12698
|
-
const architectInvalidators = [];
|
|
12699
|
-
if (owedPhase === 'architect') {
|
|
12700
|
-
if (structureChanged)
|
|
12701
|
-
architectInvalidators.push('structureHash');
|
|
12702
|
-
if (steerChanged)
|
|
12703
|
-
architectInvalidators.push('steer');
|
|
12704
|
-
if (architectChanged)
|
|
12705
|
-
architectInvalidators.push('_architect');
|
|
12706
|
-
if (crossRefsDeclChanged)
|
|
12707
|
-
architectInvalidators.push('_crossRefs');
|
|
12708
|
-
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
12709
|
-
architectInvalidators.push('architectEvery');
|
|
12710
|
-
}
|
|
12711
|
-
}
|
|
12712
|
-
// Staleness inputs
|
|
12713
|
-
const stalenessInputs = {
|
|
12714
|
-
structureHash,
|
|
12715
|
-
steerChanged,
|
|
12716
|
-
architectChanged,
|
|
12717
|
-
crossRefsDeclChanged,
|
|
12718
|
-
scopeMtimeMax: null,
|
|
12719
|
-
crossRefContentChanged: false,
|
|
12720
|
-
};
|
|
12721
12254
|
return {
|
|
12722
12255
|
path: targetNode.metaPath,
|
|
12723
12256
|
staleness: {
|
|
@@ -12791,8 +12324,8 @@ function registerQueueRoutes(app, deps) {
|
|
|
12791
12324
|
// ranked by scheduler priority (computed on read, not persisted)
|
|
12792
12325
|
let automatic = [];
|
|
12793
12326
|
try {
|
|
12794
|
-
const metaResult = await
|
|
12795
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12327
|
+
const metaResult = await deps.cache.get(deps.config, deps.watcher);
|
|
12328
|
+
const candidates = buildPhaseCandidates(metaResult.entries, deps.config.architectEvery);
|
|
12796
12329
|
const ranked = rankPhaseCandidates(candidates, deps.config.depthWeight);
|
|
12797
12330
|
automatic = ranked.map((c) => ({
|
|
12798
12331
|
path: c.node.metaPath,
|
|
@@ -12987,7 +12520,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12987
12520
|
name: SERVICE_NAME,
|
|
12988
12521
|
version: SERVICE_VERSION,
|
|
12989
12522
|
getHealth: async () => {
|
|
12990
|
-
const { config, queue, scheduler, stats, watcher } = deps;
|
|
12523
|
+
const { config, queue, scheduler, stats, watcher, cache } = deps;
|
|
12991
12524
|
// On-demand dependency checks
|
|
12992
12525
|
const [watcherHealth, gatewayHealth] = await Promise.all([
|
|
12993
12526
|
checkWatcher(config.watcherUrl),
|
|
@@ -13001,7 +12534,7 @@ function registerStatusRoute(app, deps) {
|
|
|
13001
12534
|
};
|
|
13002
12535
|
let nextPhase = null;
|
|
13003
12536
|
try {
|
|
13004
|
-
const metaResult = await
|
|
12537
|
+
const metaResult = await cache.get(config, watcher);
|
|
13005
12538
|
// Count raw phase states (before retry) for display
|
|
13006
12539
|
for (const entry of metaResult.entries) {
|
|
13007
12540
|
const ps = derivePhaseState(entry.meta);
|
|
@@ -13010,7 +12543,7 @@ function registerStatusRoute(app, deps) {
|
|
|
13010
12543
|
}
|
|
13011
12544
|
}
|
|
13012
12545
|
// Build candidates (with auto-retry) for scheduling
|
|
13013
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12546
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
13014
12547
|
// Find next phase candidate
|
|
13015
12548
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
13016
12549
|
if (winner) {
|
|
@@ -13073,7 +12606,7 @@ const synthesizeBodySchema = z.object({
|
|
|
13073
12606
|
function registerSynthesizeRoute(app, deps) {
|
|
13074
12607
|
app.post('/synthesize', async (request, reply) => {
|
|
13075
12608
|
const body = synthesizeBodySchema.parse(request.body);
|
|
13076
|
-
const { config, watcher, queue } = deps;
|
|
12609
|
+
const { config, watcher, queue, cache } = deps;
|
|
13077
12610
|
if (body.path) {
|
|
13078
12611
|
// Path-targeted trigger: create override entry
|
|
13079
12612
|
const targetPath = resolveMetaDir(body.path);
|
|
@@ -13110,7 +12643,7 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
13110
12643
|
// Corpus-wide trigger: discover stalest candidate
|
|
13111
12644
|
let result;
|
|
13112
12645
|
try {
|
|
13113
|
-
result = await
|
|
12646
|
+
result = await cache.get(config, watcher);
|
|
13114
12647
|
}
|
|
13115
12648
|
catch {
|
|
13116
12649
|
return reply.status(503).send({
|
|
@@ -13118,20 +12651,15 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
13118
12651
|
message: 'Watcher unreachable — cannot discover candidates',
|
|
13119
12652
|
});
|
|
13120
12653
|
}
|
|
13121
|
-
const
|
|
13122
|
-
|
|
13123
|
-
|
|
13124
|
-
node: e.node,
|
|
13125
|
-
meta: e.meta,
|
|
13126
|
-
actualStaleness: e.stalenessSeconds,
|
|
13127
|
-
}));
|
|
13128
|
-
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
13129
|
-
if (!stalest) {
|
|
12654
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
12655
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12656
|
+
if (!winner) {
|
|
13130
12657
|
return reply.code(200).send({
|
|
13131
12658
|
status: 'skipped',
|
|
13132
12659
|
message: 'No stale metas found. Nothing to synthesize.',
|
|
13133
12660
|
});
|
|
13134
12661
|
}
|
|
12662
|
+
const stalest = winner.node.metaPath;
|
|
13135
12663
|
const enqueueResult = queue.enqueue(stalest);
|
|
13136
12664
|
return reply.code(202).send({
|
|
13137
12665
|
status: 'accepted',
|
|
@@ -13232,6 +12760,18 @@ function createServer(options) {
|
|
|
13232
12760
|
// Fastify 5 requires `loggerInstance` for external pino loggers
|
|
13233
12761
|
const app = Fastify({
|
|
13234
12762
|
loggerInstance: options.logger,
|
|
12763
|
+
requestTimeout: 30_000,
|
|
12764
|
+
});
|
|
12765
|
+
// Readiness gate: return 503 while service is initializing
|
|
12766
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
12767
|
+
if (options.deps.ready)
|
|
12768
|
+
return;
|
|
12769
|
+
const url = request.url;
|
|
12770
|
+
if (url === '/config' || url.startsWith('/config/apply'))
|
|
12771
|
+
return;
|
|
12772
|
+
return reply
|
|
12773
|
+
.status(503)
|
|
12774
|
+
.send({ status: 'starting', message: 'Service initializing' });
|
|
13235
12775
|
});
|
|
13236
12776
|
registerRoutes(app, options.deps);
|
|
13237
12777
|
return app;
|
|
@@ -13407,8 +12947,9 @@ async function startService(config, configPath) {
|
|
|
13407
12947
|
lastCycleAt: null,
|
|
13408
12948
|
};
|
|
13409
12949
|
const queue = new SynthesisQueue(logger);
|
|
12950
|
+
const cache = new MetaCache();
|
|
13410
12951
|
// Scheduler (needs watcher for discovery)
|
|
13411
|
-
const scheduler = new Scheduler(config, queue, logger, watcher);
|
|
12952
|
+
const scheduler = new Scheduler(config, queue, logger, watcher, cache);
|
|
13412
12953
|
const routeDeps = {
|
|
13413
12954
|
config,
|
|
13414
12955
|
logger,
|
|
@@ -13416,6 +12957,8 @@ async function startService(config, configPath) {
|
|
|
13416
12957
|
watcher,
|
|
13417
12958
|
scheduler,
|
|
13418
12959
|
stats,
|
|
12960
|
+
cache,
|
|
12961
|
+
ready: false,
|
|
13419
12962
|
executor,
|
|
13420
12963
|
configPath,
|
|
13421
12964
|
};
|
|
@@ -13466,6 +13009,9 @@ async function startService(config, configPath) {
|
|
|
13466
13009
|
}
|
|
13467
13010
|
await progress.report(evt);
|
|
13468
13011
|
}, logger);
|
|
13012
|
+
// Invalidate cache only when a phase was actually executed
|
|
13013
|
+
if (result.executed)
|
|
13014
|
+
cache.invalidate();
|
|
13469
13015
|
const durationMs = Date.now() - startMs;
|
|
13470
13016
|
if (!result.executed) {
|
|
13471
13017
|
logger.debug({ path: ownerPath }, 'Phase skipped (fully fresh or locked)');
|
|
@@ -13529,9 +13075,13 @@ async function startService(config, configPath) {
|
|
|
13529
13075
|
scheduler.setRegistrar(registrar);
|
|
13530
13076
|
routeDeps.registrar = registrar;
|
|
13531
13077
|
void registrar.register().then(() => {
|
|
13078
|
+
routeDeps.ready = true;
|
|
13532
13079
|
if (registrar.isRegistered) {
|
|
13533
13080
|
void verifyRuleApplication(watcher, logger);
|
|
13534
13081
|
}
|
|
13082
|
+
}, () => {
|
|
13083
|
+
// Registration failed after max retries — mark ready anyway
|
|
13084
|
+
routeDeps.ready = true;
|
|
13535
13085
|
});
|
|
13536
13086
|
// Periodic watcher health check (independent of scheduler)
|
|
13537
13087
|
const healthCheck = new WatcherHealthCheck({
|
|
@@ -13578,11 +13128,11 @@ async function startService(config, configPath) {
|
|
|
13578
13128
|
* Parsed jeeves-meta component descriptor.
|
|
13579
13129
|
*/
|
|
13580
13130
|
const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
13581
|
-
name:
|
|
13131
|
+
name: META_COMPONENT.name,
|
|
13582
13132
|
version: SERVICE_VERSION,
|
|
13583
|
-
servicePackage:
|
|
13584
|
-
pluginPackage:
|
|
13585
|
-
defaultPort:
|
|
13133
|
+
servicePackage: META_COMPONENT.servicePackage,
|
|
13134
|
+
pluginPackage: META_COMPONENT.pluginPackage,
|
|
13135
|
+
defaultPort: META_COMPONENT.defaultPort,
|
|
13586
13136
|
// The runtime Zod custom validator only checks for a .parse() method.
|
|
13587
13137
|
// Use unknown cast to bridge the Zod v4 (service) → v3 (core SDK) type gap.
|
|
13588
13138
|
configSchema: serviceConfigSchema,
|
|
@@ -13613,4 +13163,121 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
13613
13163
|
customCliCommands: registerCustomCliCommands,
|
|
13614
13164
|
});
|
|
13615
13165
|
|
|
13616
|
-
|
|
13166
|
+
/**
|
|
13167
|
+
* Exponential moving average helper for token tracking.
|
|
13168
|
+
*
|
|
13169
|
+
* @module ema
|
|
13170
|
+
*/
|
|
13171
|
+
const DEFAULT_DECAY = 0.3;
|
|
13172
|
+
/**
|
|
13173
|
+
* Compute exponential moving average.
|
|
13174
|
+
*
|
|
13175
|
+
* @param current - New observation.
|
|
13176
|
+
* @param previous - Previous EMA value, or undefined for first observation.
|
|
13177
|
+
* @param decay - Decay factor (0-1). Higher = more weight on new value. Default 0.3.
|
|
13178
|
+
* @returns Updated EMA.
|
|
13179
|
+
*/
|
|
13180
|
+
function computeEma(current, previous, decay = DEFAULT_DECAY) {
|
|
13181
|
+
if (previous === undefined)
|
|
13182
|
+
return current;
|
|
13183
|
+
return decay * current + (1 - decay) * previous;
|
|
13184
|
+
}
|
|
13185
|
+
|
|
13186
|
+
/**
|
|
13187
|
+
* Zod schema for .meta/meta.json files.
|
|
13188
|
+
*
|
|
13189
|
+
* Reserved properties are underscore-prefixed and engine-managed.
|
|
13190
|
+
* All other keys are open schema (builder output).
|
|
13191
|
+
*
|
|
13192
|
+
* @module schema/meta
|
|
13193
|
+
*/
|
|
13194
|
+
/** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
|
|
13195
|
+
const metaJsonSchema = z
|
|
13196
|
+
.object({
|
|
13197
|
+
/** Stable identity. Auto-generated on first synthesis if not provided. */
|
|
13198
|
+
_id: z.uuid().optional(),
|
|
13199
|
+
/** Human-provided steering prompt. Optional. */
|
|
13200
|
+
_steer: z.string().optional(),
|
|
13201
|
+
/**
|
|
13202
|
+
* Explicit cross-references to other meta owner paths.
|
|
13203
|
+
* Referenced metas' _content is included as architect/builder context.
|
|
13204
|
+
*/
|
|
13205
|
+
_crossRefs: z.array(z.string()).optional(),
|
|
13206
|
+
/** Architect system prompt used this turn. Defaults from config. */
|
|
13207
|
+
_architect: z.string().optional(),
|
|
13208
|
+
/**
|
|
13209
|
+
* Task brief generated by the architect. Cached and reused across cycles;
|
|
13210
|
+
* regenerated only when triggered.
|
|
13211
|
+
*/
|
|
13212
|
+
_builder: z.string().optional(),
|
|
13213
|
+
/** Critic system prompt used this turn. Defaults from config. */
|
|
13214
|
+
_critic: z.string().optional(),
|
|
13215
|
+
/** Timestamp of last synthesis. ISO 8601. */
|
|
13216
|
+
_generatedAt: z.iso.datetime().optional(),
|
|
13217
|
+
/** Narrative synthesis output. Rendered by watcher for embedding. */
|
|
13218
|
+
_content: z.string().optional(),
|
|
13219
|
+
/**
|
|
13220
|
+
* Hash of sorted file listing in scope. Detects directory structure
|
|
13221
|
+
* changes that trigger an architect re-run.
|
|
13222
|
+
*/
|
|
13223
|
+
_structureHash: z.string().optional(),
|
|
13224
|
+
/**
|
|
13225
|
+
* Cycles since last architect run. Reset to 0 when architect runs.
|
|
13226
|
+
* Used with architectEvery to trigger periodic re-prompting.
|
|
13227
|
+
*/
|
|
13228
|
+
_synthesisCount: z.number().int().min(0).optional(),
|
|
13229
|
+
/** Critic evaluation of the last synthesis. */
|
|
13230
|
+
_feedback: z.string().optional(),
|
|
13231
|
+
/**
|
|
13232
|
+
* Present and true on archive snapshots. Distinguishes live vs. archived
|
|
13233
|
+
* metas.
|
|
13234
|
+
*/
|
|
13235
|
+
_archived: z.boolean().optional(),
|
|
13236
|
+
/** Timestamp when this snapshot was archived. ISO 8601. */
|
|
13237
|
+
_archivedAt: z.iso.datetime().optional(),
|
|
13238
|
+
/**
|
|
13239
|
+
* Scheduling priority. Higher = updates more often. Negative allowed;
|
|
13240
|
+
* normalized to min 0 at scheduling time.
|
|
13241
|
+
*/
|
|
13242
|
+
_depth: z.number().optional(),
|
|
13243
|
+
/**
|
|
13244
|
+
* Emphasis multiplier for depth weighting in scheduling.
|
|
13245
|
+
* Default 1. Higher values increase this meta's scheduling priority
|
|
13246
|
+
* relative to its depth. Set to 0.5 to halve the depth effect,
|
|
13247
|
+
* 2 to double it, 0 to ignore depth entirely for this meta.
|
|
13248
|
+
*/
|
|
13249
|
+
_emphasis: z.number().min(0).optional(),
|
|
13250
|
+
/** Token count from last architect subprocess call. */
|
|
13251
|
+
_architectTokens: z.number().int().optional(),
|
|
13252
|
+
/** Token count from last builder subprocess call. */
|
|
13253
|
+
_builderTokens: z.number().int().optional(),
|
|
13254
|
+
/** Token count from last critic subprocess call. */
|
|
13255
|
+
_criticTokens: z.number().int().optional(),
|
|
13256
|
+
/** Exponential moving average of architect token usage (decay 0.3). */
|
|
13257
|
+
_architectTokensAvg: z.number().optional(),
|
|
13258
|
+
/** Exponential moving average of builder token usage (decay 0.3). */
|
|
13259
|
+
_builderTokensAvg: z.number().optional(),
|
|
13260
|
+
/** Exponential moving average of critic token usage (decay 0.3). */
|
|
13261
|
+
_criticTokensAvg: z.number().optional(),
|
|
13262
|
+
/**
|
|
13263
|
+
* Opaque state carried across synthesis cycles for progressive work.
|
|
13264
|
+
* Set by the builder, passed back as context on next cycle.
|
|
13265
|
+
*/
|
|
13266
|
+
_state: z.unknown().optional(),
|
|
13267
|
+
/**
|
|
13268
|
+
* Structured error from last cycle. Present when a step failed.
|
|
13269
|
+
* Cleared on successful cycle.
|
|
13270
|
+
*/
|
|
13271
|
+
_error: metaErrorSchema.optional(),
|
|
13272
|
+
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
13273
|
+
_disabled: z.boolean().optional(),
|
|
13274
|
+
/**
|
|
13275
|
+
* Per-phase state machine record. Engine-managed.
|
|
13276
|
+
* Keyed by phase name (architect, builder, critic) with status values.
|
|
13277
|
+
* Persisted to survive ticks; derived on first load for back-compat.
|
|
13278
|
+
*/
|
|
13279
|
+
_phaseState: phaseStateSchema.optional(),
|
|
13280
|
+
})
|
|
13281
|
+
.loose();
|
|
13282
|
+
|
|
13283
|
+
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestratePhase, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, runArchitect, runBuilder, runCritic, serviceConfigSchema, sleepAsync as sleep, startService, toMetaError, verifyRuleApplication };
|