@ngockhoale/ukit 3.0.2 → 3.0.4
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/CHANGELOG.md +21 -0
- package/bin/ukit +12 -4
- package/manifests/engineConformance.yaml +29 -0
- package/manifests/platform.full.yaml +13 -0
- package/package.json +2 -1
- package/scripts/audit/decision-coverage.mjs +295 -0
- package/scripts/bench/outline-savings.mjs +19 -4
- package/scripts/bench/parallel-agents.mjs +15 -4
- package/scripts/bench/runGold.mjs +22 -4
- package/scripts/bench/v3-ceremony.mjs +7 -1
- package/scripts/bug/triage.mjs +56 -17
- package/scripts/index/build-index.mjs +94 -28
- package/scripts/index/query-index.mjs +48 -14
- package/scripts/index/refresh-index.mjs +142 -62
- package/scripts/perf/audit-perf.mjs +8 -2
- package/scripts/skill/audit-skill.mjs +54 -25
- package/src/bug/triageBug.js +9 -6
- package/src/cli/adapters.js +6 -0
- package/src/cli/commands/code.js +7 -1
- package/src/cli/commands/indexArgs.js +4 -2
- package/src/cli/commands/indexTools.js +8 -1
- package/src/cli/commands/install.js +13 -0
- package/src/cli/commands/memory.js +10 -3
- package/src/cli/commands/status.js +17 -1
- package/src/cli/commands/update.js +7 -0
- package/src/context/detectProjectContext.js +3 -1
- package/src/core/codeintel/analogy.js +1 -1
- package/src/core/codeintel/diagnostics.js +9 -6
- package/src/core/codeintel/graph.js +14 -8
- package/src/core/codeintel/impact.js +0 -1
- package/src/core/codeintel/invalidation.js +5 -3
- package/src/core/codeintel/packet.js +11 -0
- package/src/core/codeintel/router.js +17 -3
- package/src/core/codeintel/semanticProvider.js +1 -1
- package/src/core/codeintel/summaries.js +11 -7
- package/src/core/compact/contextBudget.js +26 -12
- package/src/core/compact/index.js +15 -8
- package/src/core/docContracts.js +10 -2
- package/src/core/experiments/deliberation.js +321 -0
- package/src/core/experiments/dynamicWorkflow.js +492 -0
- package/src/core/fileOps.js +8 -1
- package/src/core/gatewayProbe.js +29 -1
- package/src/core/gatewayResilienceEnv.js +44 -3
- package/src/core/handoffDocValidator.js +3 -1
- package/src/core/hookChainDoctor.js +16 -1
- package/src/core/memory/deltaOverlays.js +448 -0
- package/src/core/memory/learningCandidates.js +302 -0
- package/src/core/memory/migrate.js +59 -32
- package/src/core/memory/recordStore.js +24 -1
- package/src/core/memory/store.js +44 -8
- package/src/core/memory/storeV2.js +48 -33
- package/src/core/memory/userMemory.js +11 -10
- package/src/core/output/index.js +15 -10
- package/src/core/permissionPolicy.js +8 -0
- package/src/core/runtimeConfig.js +224 -4
- package/src/core/sensitiveValueScanner.js +10 -2
- package/src/core/taskBudgetValidator.js +12 -17
- package/src/core/taskProgressGuard.js +59 -9
- package/src/core/unattendedDoctor.js +5 -2
- package/src/core/uninstall.js +37 -8
- package/src/decision/client.js +371 -0
- package/src/decision/lease.js +198 -0
- package/src/decision/preflight.js +492 -0
- package/src/decision/protocol.js +308 -0
- package/src/decision/registry.js +384 -0
- package/src/decision/shadow.js +281 -0
- package/src/decision/statePacket.js +165 -0
- package/src/diagnostics/failurePatterns.js +2 -1
- package/src/diagnostics/ledgerFiles.js +3 -1
- package/src/diagnostics/routeOutcomes.js +1 -29
- package/src/index/buildIndex.js +23 -11
- package/src/index/impactContext.js +21 -2
- package/src/index/importResolution.js +13 -7
- package/src/index/queryIndex.js +11 -5
- package/src/index/resolveContext.js +16 -6
- package/src/index/taskRouting.js +63 -2
- package/src/index/verificationPlan.js +12 -1
- package/src/learning/patternProposals.js +6 -0
- package/src/render/renderTemplate.js +1 -1
- package/src/skill/auditSkill.js +3 -1
- package/src/stack/detectStack.js +3 -1
- package/template_project/.claude/agents/handoff-planner.md +2 -5
- package/template_project/.claude/hooks/auto-allow-bash.sh +5 -0
- package/template_project/.claude/hooks/block-dangerous.mjs +11 -4
- package/template_project/.claude/hooks/context-hardcap-gate.sh +10 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +14 -4
- package/template_project/.claude/hooks/handoff-resume.sh +10 -1
- package/template_project/.claude/hooks/protect-files.sh +0 -1
- package/template_project/.claude/hooks/record-execution.mjs +13 -1
- package/template_project/.claude/hooks/sensitive-data-guard.mjs +71 -5
- package/template_project/.claude/hooks/session-episode.sh +9 -2
- package/template_project/.claude/skills/pdf-processing-pro/SKILL.md +1 -1
- package/template_project/.claude/ukit/index/handoff-doc-validator.mjs +3 -1
- package/template_project/.claude/ukit/index/lib/index-core.mjs +123 -39
- package/template_project/.claude/ukit/index/route-task.mjs +444 -0
- package/template_project/.claude/ukit/index/task-budget-validator.mjs +12 -16
- package/template_project/.claude/ukit/index/unic-decision.mjs +786 -0
- package/template_project/.claude/ukit/index/verify-context.mjs +11 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +116 -9
- package/template_project/.claude/ukit/runtime/project-important.mjs +9 -7
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +48 -0
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +596 -0
- package/template_project/.claude/ukit/runtime/sensitive-value-scanner.mjs +4 -7
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -1
- package/template_project/docs/AI_HANDOFF/PLAN.md +7 -7
- package/template_project/docs/AI_HANDOFF/RULES.md +1 -1
- package/template_project/ukit/storage/config.json +34 -1
- package/template_project/.claude/ukit/skill-router-state.json +0 -1
- package/template_project/.ukit/storage/cache/hook-latency/unknown.jsonl +0 -4
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// ({ schemaVersion: 2, records: [...] }); writes via fileOps.writeJson (tmp+rename).
|
|
4
4
|
// Read ops call ensureMigrated() first so every consumer triggers the lazy
|
|
5
5
|
// v1→v2 migration (SPEC §4); migrate.js is lazy-imported to break the
|
|
6
|
-
// storeV2↔migrate import cycle
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// storeV2↔migrate import cycle. The record-document read/write core lives in
|
|
7
|
+
// recordStore.js (FR-007) so the user-level store (userMemory.js) shares it
|
|
8
|
+
// without the migration flow; mutations go through mutateRecordStore, which
|
|
9
|
+
// serializes read-modify-write under the shared file lock.
|
|
9
10
|
|
|
10
|
-
import fs from 'node:fs/promises';
|
|
11
11
|
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
12
12
|
import { loadRuntimeConfig } from '../runtimeConfig.js';
|
|
13
13
|
import { createRecord, normalizeRecord } from './records.js';
|
|
@@ -16,35 +16,44 @@ import {
|
|
|
16
16
|
writeRecordStore,
|
|
17
17
|
applyRecordPatch,
|
|
18
18
|
isRawRecord,
|
|
19
|
+
mutateRecordStore,
|
|
19
20
|
} from './recordStore.js';
|
|
20
21
|
|
|
21
|
-
// Per-process memoization for ensureMigrated — one-
|
|
22
|
-
|
|
22
|
+
// Per-process memoization for ensureMigrated — one in-flight migration per
|
|
23
|
+
// projectRoot. The PROMISE is cached (not a done-flag): concurrent callers
|
|
24
|
+
// during a migration must await the same run — a done-flag set up front let
|
|
25
|
+
// a second reader see an empty store mid-migration, and a writer could create
|
|
26
|
+
// records.json which the in-flight migration then clobbered.
|
|
27
|
+
const migrationPromises = new Map();
|
|
28
|
+
|
|
29
|
+
|
|
23
30
|
|
|
24
31
|
async function ensureMigrated(projectRoot) {
|
|
25
32
|
const key = String(projectRoot);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const paths = buildRuntimePaths(projectRoot);
|
|
30
|
-
let recordsExists = false;
|
|
31
|
-
try {
|
|
32
|
-
await fs.access(paths.memoryV2RecordsPath);
|
|
33
|
-
recordsExists = true;
|
|
34
|
-
} catch {
|
|
35
|
-
recordsExists = false;
|
|
36
|
-
}
|
|
37
|
-
if (recordsExists) return;
|
|
33
|
+
const existing = migrationPromises.get(key);
|
|
34
|
+
if (existing) return existing;
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const promise = (async () => {
|
|
37
|
+
try {
|
|
38
|
+
const config = await loadRuntimeConfig(projectRoot);
|
|
39
|
+
if (config?.memoryV2?.autoMigrate === false) return;
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const migrate = await import('./migrate.js');
|
|
42
|
+
// needsMigration checks the marker — NOT records.json. Gating on
|
|
43
|
+
// records.json stranded v1 data forever whenever the file existed
|
|
44
|
+
// without the marker (a pre-migration write, a crashed first run).
|
|
45
|
+
if (await migrate.needsMigration(projectRoot)) {
|
|
46
|
+
await migrate.runMigration(projectRoot);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Lazy migration is best-effort: a failed auto-run must not break reads.
|
|
45
50
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
51
|
+
})();
|
|
52
|
+
migrationPromises.set(key, promise);
|
|
53
|
+
try {
|
|
54
|
+
await promise;
|
|
55
|
+
} finally {
|
|
56
|
+
migrationPromises.delete(key);
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
@@ -73,6 +82,8 @@ export async function saveRecords(projectRoot, records) {
|
|
|
73
82
|
/**
|
|
74
83
|
* addRecord(projectRoot, recordInput) → record
|
|
75
84
|
* Accepts either createRecord-style input or a full raw record (normalized).
|
|
85
|
+
* Runs ensureMigrated first — a first v2 write must not create records.json
|
|
86
|
+
* ahead of the v1→v2 migration and strand legacy data.
|
|
76
87
|
*/
|
|
77
88
|
export async function addRecord(projectRoot, recordInput) {
|
|
78
89
|
const record = isRawRecord(recordInput)
|
|
@@ -81,9 +92,13 @@ export async function addRecord(projectRoot, recordInput) {
|
|
|
81
92
|
if (!record) {
|
|
82
93
|
throw new Error('addRecord: input could not be normalized into a valid record');
|
|
83
94
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
await ensureMigrated(projectRoot);
|
|
96
|
+
const paths = buildRuntimePaths(projectRoot);
|
|
97
|
+
const stored = await mutateRecordStore(paths.memoryV2RecordsPath, (records) => ({
|
|
98
|
+
result: record,
|
|
99
|
+
records: [...records, record],
|
|
100
|
+
}));
|
|
101
|
+
return stored ?? record;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
/**
|
|
@@ -92,11 +107,11 @@ export async function addRecord(projectRoot, recordInput) {
|
|
|
92
107
|
export async function updateRecord(projectRoot, id, patch = {}) {
|
|
93
108
|
await ensureMigrated(projectRoot);
|
|
94
109
|
const paths = buildRuntimePaths(projectRoot);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
return mutateRecordStore(paths.memoryV2RecordsPath, (records) => {
|
|
111
|
+
const result = applyRecordPatch(records, id, patch);
|
|
112
|
+
if (!result) return null;
|
|
113
|
+
return { result: result.record, records: result.records };
|
|
114
|
+
});
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
/**
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
writeRecordStore,
|
|
12
12
|
applyRecordPatch,
|
|
13
13
|
isRawRecord,
|
|
14
|
+
mutateRecordStore,
|
|
14
15
|
} from './recordStore.js';
|
|
15
16
|
import { loadRecords } from './storeV2.js';
|
|
16
17
|
|
|
@@ -44,22 +45,22 @@ export async function addUserRecord(recordInput, { homeDir } = {}) {
|
|
|
44
45
|
if (!record) {
|
|
45
46
|
throw new Error('addUserRecord: input could not be normalized into a valid record');
|
|
46
47
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const stored = await mutateRecordStore(userRecordsPath(homeDir), (records) => ({
|
|
49
|
+
result: record,
|
|
50
|
+
records: [...records, record],
|
|
51
|
+
}));
|
|
52
|
+
return stored ?? record;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* updateUserRecord(id, patch, {homeDir}={}) → record | null
|
|
55
57
|
*/
|
|
56
58
|
export async function updateUserRecord(id, patch = {}, { homeDir } = {}) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return result.record;
|
|
59
|
+
return mutateRecordStore(userRecordsPath(homeDir), (records) => {
|
|
60
|
+
const result = applyRecordPatch(records, id, patch);
|
|
61
|
+
if (!result) return null;
|
|
62
|
+
return { result: result.record, records: result.records };
|
|
63
|
+
});
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
package/src/core/output/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { readJsonIfExists, writeJson, withFileLock } from '../fileOps.js';
|
|
3
|
+
import { readJsonIfExists, writeJson, withFileLock, escapeRegExp } from '../fileOps.js';
|
|
4
4
|
import { recordCompaction } from '../compact/index.js';
|
|
5
5
|
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
6
6
|
import {
|
|
@@ -202,9 +202,6 @@ function detectOutputProfile(command) {
|
|
|
202
202
|
return OUTPUT_PROFILES.find((profile) => matchesAnyPattern(commandText, profile.commandPatterns)) ?? null;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
function escapeRegExp(value) {
|
|
206
|
-
return String(value ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
207
|
-
}
|
|
208
205
|
|
|
209
206
|
function buildProjectRootVariants(projectRoot) {
|
|
210
207
|
const trimmed = String(projectRoot ?? '').trim();
|
|
@@ -381,16 +378,24 @@ export async function sweepTeeCache(dir, {
|
|
|
381
378
|
maxEntries = TEE_SWEEP_MAX_ENTRIES,
|
|
382
379
|
maxRemovals = TEE_SWEEP_MAX_REMOVALS,
|
|
383
380
|
} = {}) {
|
|
384
|
-
let
|
|
381
|
+
let dirents;
|
|
385
382
|
try {
|
|
386
|
-
|
|
383
|
+
dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
387
384
|
} catch {
|
|
388
385
|
return { sampled: true, scanned: 0, removed: 0 };
|
|
389
386
|
}
|
|
387
|
+
// Eligible-file count comes from the dirents (one bounded readdir, no per-name
|
|
388
|
+
// stat) so the count cap is enforceable even when maxFiles > maxEntries —
|
|
389
|
+
// mirror of BUG-C22-10 in the runtime twin: capping `scanned` at maxEntries
|
|
390
|
+
// made `entries.length` an under-count and `overflow` was always 0.
|
|
391
|
+
const eligibleNames = [];
|
|
392
|
+
for (const dirent of dirents) {
|
|
393
|
+
if (dirent.isFile()) eligibleNames.push(dirent.name);
|
|
394
|
+
}
|
|
390
395
|
const cutoff = now() - maxAgeMs;
|
|
391
396
|
const entries = [];
|
|
392
397
|
let scanned = 0;
|
|
393
|
-
for (const name of
|
|
398
|
+
for (const name of eligibleNames) {
|
|
394
399
|
if (scanned >= maxEntries) break;
|
|
395
400
|
scanned += 1;
|
|
396
401
|
try {
|
|
@@ -400,9 +405,9 @@ export async function sweepTeeCache(dir, {
|
|
|
400
405
|
}
|
|
401
406
|
entries.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
402
407
|
// Overflow counts eligible FILES only — foreign entries (non-matching names,
|
|
403
|
-
// dirs) inflate
|
|
408
|
+
// dirs) inflate the dir listing and would evict real entries while the dir is
|
|
404
409
|
// actually under the cap.
|
|
405
|
-
const overflow = Math.max(0,
|
|
410
|
+
const overflow = Math.max(0, eligibleNames.length - maxFiles);
|
|
406
411
|
let removed = 0;
|
|
407
412
|
for (const entry of entries) {
|
|
408
413
|
if (removed >= maxRemovals) break;
|
|
@@ -1148,7 +1153,7 @@ export async function readOutputHistory(projectRoot, options = {}) {
|
|
|
1148
1153
|
|
|
1149
1154
|
export async function appendOutputHistory(projectRoot, entry, options = {}) {
|
|
1150
1155
|
const normalizedEntry = normalizeOutputHistoryEntry(entry);
|
|
1151
|
-
if (!normalizedEntry || !normalizedEntry.
|
|
1156
|
+
if (!normalizedEntry || !normalizedEntry.summary) {
|
|
1152
1157
|
return readOutputHistory(projectRoot, options);
|
|
1153
1158
|
}
|
|
1154
1159
|
|
|
@@ -102,6 +102,14 @@ export function compilePermissionPolicy(input = {}) {
|
|
|
102
102
|
if (surfaces === null || typeof surfaces !== 'object' || Array.isArray(surfaces)) {
|
|
103
103
|
throw policyError('surfaces must be an object when provided');
|
|
104
104
|
}
|
|
105
|
+
// Fail closed on malformed surface lists: a string iterates into per-char
|
|
106
|
+
// rules and a number throws a bare TypeError — both bypass the policy
|
|
107
|
+
// contract. Only arrays (or absent keys) are legal.
|
|
108
|
+
for (const key of ['protect', 'deny', 'allow', 'ask']) {
|
|
109
|
+
if (surfaces[key] !== undefined && !Array.isArray(surfaces[key])) {
|
|
110
|
+
throw policyError(`surfaces.${key} must be an array when provided, got: ${JSON.stringify(surfaces[key])}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
105
113
|
|
|
106
114
|
const mode = permissionMode;
|
|
107
115
|
const effectiveMode = mode;
|
|
@@ -183,6 +183,36 @@ function pushNonEmptyStringError(errors, value, label) {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// Reserved rollout stages for every feature-stage key (routing.*, decisionPlane.*,
|
|
187
|
+
// continuity.*, learning.*). Stages promote off → shadow → canary → default;
|
|
188
|
+
// absence always means 'off'. Single source of truth — validators and stage
|
|
189
|
+
// resolvers share this set.
|
|
190
|
+
export const VALID_ROUTE_STAGES = new Set(['off', 'shadow', 'canary', 'default']);
|
|
191
|
+
|
|
192
|
+
// Generic stage resolver for dotted paths like 'decisionPlane.stage' or
|
|
193
|
+
// 'continuity.resumableRun.stage'. Absent config, absent key, non-object
|
|
194
|
+
// intermediate, or malformed stage value all resolve to 'off' — a bad config
|
|
195
|
+
// can never promote a feature. The validator reports the same malformed value
|
|
196
|
+
// as an error, so 'off' here is the safe fallback, not silent acceptance.
|
|
197
|
+
export function resolveConfigStage(config = null, path) {
|
|
198
|
+
const keys = String(path ?? '').split('.');
|
|
199
|
+
let node = config;
|
|
200
|
+
for (const key of keys) {
|
|
201
|
+
if (!isPlainObject(node)) return 'off';
|
|
202
|
+
node = node[key];
|
|
203
|
+
}
|
|
204
|
+
return VALID_ROUTE_STAGES.has(node) ? node : 'off';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Pushes a stage-enum error when `node.stage` is present but not reserved.
|
|
208
|
+
// Absent stage is valid (absence = 'off'); a non-object node is reported by
|
|
209
|
+
// the caller's own "must be an object" check.
|
|
210
|
+
function pushStageError(errors, node, label) {
|
|
211
|
+
if (node.stage !== undefined && !VALID_ROUTE_STAGES.has(node.stage)) {
|
|
212
|
+
errors.push(`${label}.stage must be one of: ${[...VALID_ROUTE_STAGES].join(', ')}.`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
186
216
|
export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
187
217
|
const safeOverrides = isPlainObject(overrides) ? overrides : {};
|
|
188
218
|
|
|
@@ -263,6 +293,36 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
263
293
|
fastPath: { stage: 'off' },
|
|
264
294
|
escalation: { stage: 'off' },
|
|
265
295
|
},
|
|
296
|
+
// C52 M07 UNIC Decision Agent (docs/pstack/UNIC_DECISION_SPEC §17). All
|
|
297
|
+
// stages ship 'off' — zero outbound calls, deterministic policy stays
|
|
298
|
+
// authoritative. `enabled:false` is the global emergency disable.
|
|
299
|
+
decisionPlane: {
|
|
300
|
+
enabled: true,
|
|
301
|
+
stage: 'off',
|
|
302
|
+
logicalModel: 'unic-decision',
|
|
303
|
+
checkpoints: {
|
|
304
|
+
default: 'unic-decision/laya-multilingual',
|
|
305
|
+
english: 'unic-decision/laya',
|
|
306
|
+
unknownLanguage: 'unic-decision/auto',
|
|
307
|
+
},
|
|
308
|
+
timeoutMs: 5000,
|
|
309
|
+
maxStateTokens: { multilingual: 1024, english: 512 },
|
|
310
|
+
maxEnumLabels: 11,
|
|
311
|
+
maxBatchQuestions: 8,
|
|
312
|
+
preflightRequired: true,
|
|
313
|
+
portableReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
314
|
+
leaseChoices: [1, 2, 5, 10],
|
|
315
|
+
cachePolicy: {
|
|
316
|
+
decideBeforeLane: true,
|
|
317
|
+
freezeCacheSensitiveFields: true,
|
|
318
|
+
requireVerifiedNativeUpdate: true,
|
|
319
|
+
},
|
|
320
|
+
circuitBreaker: { failureThreshold: 3, cooldownMs: 30_000 },
|
|
321
|
+
// Per-family stage overrides (preflight, route, capability, verify,
|
|
322
|
+
// resume, learn, …) — absence = 'off'; names are declared by the
|
|
323
|
+
// decision registry, not here.
|
|
324
|
+
families: {},
|
|
325
|
+
},
|
|
266
326
|
// SPEC-model-roles: role→tier map injected into delegating-lane route output.
|
|
267
327
|
// Values are tier names or 'inherit-parent' — never raw provider model IDs.
|
|
268
328
|
modelRoles: {
|
|
@@ -360,6 +420,25 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
360
420
|
proposals: { minCount: 3, minSessions: 2 },
|
|
361
421
|
episodes: { autoWrite: false },
|
|
362
422
|
tuning: { enabled: true, applyMode: 'manual' },
|
|
423
|
+
// C52 M04.2 stage keys (SPEC §5 FR-016–FR-018). candidates: repeated
|
|
424
|
+
// corrections/suppressions/escalations promote to a LearningCandidate
|
|
425
|
+
// only after minOccurrences + cross-session evidence; report-only at
|
|
426
|
+
// 'shadow'. overlays: delta overlays on policy fields; base-version
|
|
427
|
+
// mismatch → conflict, never silent apply. Promotion stays manual.
|
|
428
|
+
candidates: { stage: 'off', minOccurrences: 3 },
|
|
429
|
+
overlays: { stage: 'off' },
|
|
430
|
+
},
|
|
431
|
+
// C52 M04.1 compact resumable state (SPEC §5 FR-012–FR-015). Stage 'off'
|
|
432
|
+
// → no resumable-run records written or consumed; resume falls back to
|
|
433
|
+
// the existing reinject path.
|
|
434
|
+
continuity: {
|
|
435
|
+
resumableRun: { stage: 'off' },
|
|
436
|
+
},
|
|
437
|
+
// C52 M06 experiments (SPEC §5 FR-021–FR-023). Disabled by default —
|
|
438
|
+
// zero calls and zero prompt content when off; no auto-promotion.
|
|
439
|
+
experiments: {
|
|
440
|
+
deliberation: { enabled: false },
|
|
441
|
+
dynamicWorkflow: { enabled: false, maxAttemptsPerFailure: 2, noProgressCap: 3 },
|
|
363
442
|
},
|
|
364
443
|
safePatch: {
|
|
365
444
|
enabled: true,
|
|
@@ -539,7 +618,6 @@ export function validateRuntimeConfig(config) {
|
|
|
539
618
|
if (!isPlainObject(config.routing)) {
|
|
540
619
|
errors.push('routing must be an object.');
|
|
541
620
|
} else {
|
|
542
|
-
const VALID_ROUTE_STAGES = new Set(['off', 'shadow', 'canary', 'default']);
|
|
543
621
|
for (const key of ['routeSchema', 'rigor', 'fastPath', 'escalation']) {
|
|
544
622
|
const stageKey = config.routing[key];
|
|
545
623
|
if (stageKey === undefined) {
|
|
@@ -547,9 +625,133 @@ export function validateRuntimeConfig(config) {
|
|
|
547
625
|
}
|
|
548
626
|
if (!isPlainObject(stageKey)) {
|
|
549
627
|
errors.push(`routing.${key} must be an object.`);
|
|
550
|
-
} else
|
|
551
|
-
|
|
552
|
-
|
|
628
|
+
} else {
|
|
629
|
+
pushStageError(errors, stageKey, `routing.${key}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// C52 M07 decisionPlane — optional-present, additive-namespaced. Absent →
|
|
636
|
+
// valid (pre-C52 configs); present → stage keys must hold reserved stages,
|
|
637
|
+
// present fields must be well-typed. families.* accepts arbitrary family
|
|
638
|
+
// names (declared by the decision registry) but every family stage is
|
|
639
|
+
// enum-checked.
|
|
640
|
+
if (config.decisionPlane !== undefined) {
|
|
641
|
+
if (!isPlainObject(config.decisionPlane)) {
|
|
642
|
+
errors.push('decisionPlane must be an object.');
|
|
643
|
+
} else {
|
|
644
|
+
const decisionPlane = config.decisionPlane;
|
|
645
|
+
if (decisionPlane.enabled !== undefined) {
|
|
646
|
+
pushBooleanError(errors, decisionPlane.enabled, 'decisionPlane.enabled');
|
|
647
|
+
}
|
|
648
|
+
pushStageError(errors, decisionPlane, 'decisionPlane');
|
|
649
|
+
if (decisionPlane.timeoutMs !== undefined) {
|
|
650
|
+
pushPositiveNumberError(errors, decisionPlane.timeoutMs, 'decisionPlane.timeoutMs');
|
|
651
|
+
}
|
|
652
|
+
if (decisionPlane.maxEnumLabels !== undefined) {
|
|
653
|
+
pushPositiveNumberError(errors, decisionPlane.maxEnumLabels, 'decisionPlane.maxEnumLabels');
|
|
654
|
+
}
|
|
655
|
+
if (decisionPlane.maxBatchQuestions !== undefined) {
|
|
656
|
+
pushPositiveNumberError(errors, decisionPlane.maxBatchQuestions, 'decisionPlane.maxBatchQuestions');
|
|
657
|
+
}
|
|
658
|
+
if (decisionPlane.preflightRequired !== undefined) {
|
|
659
|
+
pushBooleanError(errors, decisionPlane.preflightRequired, 'decisionPlane.preflightRequired');
|
|
660
|
+
}
|
|
661
|
+
if (decisionPlane.maxStateTokens !== undefined) {
|
|
662
|
+
if (!isPlainObject(decisionPlane.maxStateTokens)) {
|
|
663
|
+
errors.push('decisionPlane.maxStateTokens must be an object.');
|
|
664
|
+
} else {
|
|
665
|
+
pushPositiveNumberError(errors, decisionPlane.maxStateTokens.multilingual, 'decisionPlane.maxStateTokens.multilingual');
|
|
666
|
+
pushPositiveNumberError(errors, decisionPlane.maxStateTokens.english, 'decisionPlane.maxStateTokens.english');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (decisionPlane.checkpoints !== undefined) {
|
|
670
|
+
if (!isPlainObject(decisionPlane.checkpoints)) {
|
|
671
|
+
errors.push('decisionPlane.checkpoints must be an object.');
|
|
672
|
+
} else {
|
|
673
|
+
for (const key of ['default', 'english', 'unknownLanguage']) {
|
|
674
|
+
if (decisionPlane.checkpoints[key] !== undefined) {
|
|
675
|
+
pushNonEmptyStringError(errors, decisionPlane.checkpoints[key], `decisionPlane.checkpoints.${key}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (decisionPlane.leaseChoices !== undefined && !Array.isArray(decisionPlane.leaseChoices)) {
|
|
681
|
+
errors.push('decisionPlane.leaseChoices must be an array.');
|
|
682
|
+
}
|
|
683
|
+
if (decisionPlane.portableReasoningEfforts !== undefined
|
|
684
|
+
&& !Array.isArray(decisionPlane.portableReasoningEfforts)) {
|
|
685
|
+
errors.push('decisionPlane.portableReasoningEfforts must be an array.');
|
|
686
|
+
}
|
|
687
|
+
if (decisionPlane.cachePolicy !== undefined && !isPlainObject(decisionPlane.cachePolicy)) {
|
|
688
|
+
errors.push('decisionPlane.cachePolicy must be an object.');
|
|
689
|
+
}
|
|
690
|
+
if (decisionPlane.circuitBreaker !== undefined) {
|
|
691
|
+
if (!isPlainObject(decisionPlane.circuitBreaker)) {
|
|
692
|
+
errors.push('decisionPlane.circuitBreaker must be an object.');
|
|
693
|
+
} else {
|
|
694
|
+
if (decisionPlane.circuitBreaker.failureThreshold !== undefined) {
|
|
695
|
+
pushPositiveNumberError(errors, decisionPlane.circuitBreaker.failureThreshold, 'decisionPlane.circuitBreaker.failureThreshold');
|
|
696
|
+
}
|
|
697
|
+
if (decisionPlane.circuitBreaker.cooldownMs !== undefined) {
|
|
698
|
+
pushPositiveNumberError(errors, decisionPlane.circuitBreaker.cooldownMs, 'decisionPlane.circuitBreaker.cooldownMs');
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (decisionPlane.families !== undefined) {
|
|
703
|
+
if (!isPlainObject(decisionPlane.families)) {
|
|
704
|
+
errors.push('decisionPlane.families must be an object.');
|
|
705
|
+
} else {
|
|
706
|
+
for (const [family, familyConfig] of Object.entries(decisionPlane.families)) {
|
|
707
|
+
if (!isPlainObject(familyConfig)) {
|
|
708
|
+
errors.push(`decisionPlane.families.${family} must be an object.`);
|
|
709
|
+
} else {
|
|
710
|
+
pushStageError(errors, familyConfig, `decisionPlane.families.${family}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// C52 M04.1 continuity — optional-present; resumableRun carries one stage key.
|
|
719
|
+
if (config.continuity !== undefined) {
|
|
720
|
+
if (!isPlainObject(config.continuity)) {
|
|
721
|
+
errors.push('continuity must be an object.');
|
|
722
|
+
} else if (config.continuity.resumableRun !== undefined) {
|
|
723
|
+
if (!isPlainObject(config.continuity.resumableRun)) {
|
|
724
|
+
errors.push('continuity.resumableRun must be an object.');
|
|
725
|
+
} else {
|
|
726
|
+
pushStageError(errors, config.continuity.resumableRun, 'continuity.resumableRun');
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// C52 M06 experiments — optional-present; both experiments are
|
|
732
|
+
// disabled-by-default booleans plus bounded breakers.
|
|
733
|
+
if (config.experiments !== undefined) {
|
|
734
|
+
if (!isPlainObject(config.experiments)) {
|
|
735
|
+
errors.push('experiments must be an object.');
|
|
736
|
+
} else {
|
|
737
|
+
if (config.experiments.deliberation !== undefined) {
|
|
738
|
+
if (!isPlainObject(config.experiments.deliberation)) {
|
|
739
|
+
errors.push('experiments.deliberation must be an object.');
|
|
740
|
+
} else {
|
|
741
|
+
pushBooleanError(errors, config.experiments.deliberation.enabled, 'experiments.deliberation.enabled');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (config.experiments.dynamicWorkflow !== undefined) {
|
|
745
|
+
if (!isPlainObject(config.experiments.dynamicWorkflow)) {
|
|
746
|
+
errors.push('experiments.dynamicWorkflow must be an object.');
|
|
747
|
+
} else {
|
|
748
|
+
pushBooleanError(errors, config.experiments.dynamicWorkflow.enabled, 'experiments.dynamicWorkflow.enabled');
|
|
749
|
+
if (config.experiments.dynamicWorkflow.maxAttemptsPerFailure !== undefined) {
|
|
750
|
+
pushPositiveNumberError(errors, config.experiments.dynamicWorkflow.maxAttemptsPerFailure, 'experiments.dynamicWorkflow.maxAttemptsPerFailure');
|
|
751
|
+
}
|
|
752
|
+
if (config.experiments.dynamicWorkflow.noProgressCap !== undefined) {
|
|
753
|
+
pushPositiveNumberError(errors, config.experiments.dynamicWorkflow.noProgressCap, 'experiments.dynamicWorkflow.noProgressCap');
|
|
754
|
+
}
|
|
553
755
|
}
|
|
554
756
|
}
|
|
555
757
|
}
|
|
@@ -797,6 +999,24 @@ export function validateRuntimeConfig(config) {
|
|
|
797
999
|
}
|
|
798
1000
|
}
|
|
799
1001
|
}
|
|
1002
|
+
// C52 M04.2 stage keys — same optional-present contract as routing.*.
|
|
1003
|
+
if (learning.candidates !== undefined) {
|
|
1004
|
+
if (!isPlainObject(learning.candidates)) {
|
|
1005
|
+
errors.push('learning.candidates must be an object.');
|
|
1006
|
+
} else {
|
|
1007
|
+
pushStageError(errors, learning.candidates, 'learning.candidates');
|
|
1008
|
+
if (learning.candidates.minOccurrences !== undefined) {
|
|
1009
|
+
pushPositiveNumberError(errors, learning.candidates.minOccurrences, 'learning.candidates.minOccurrences');
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (learning.overlays !== undefined) {
|
|
1014
|
+
if (!isPlainObject(learning.overlays)) {
|
|
1015
|
+
errors.push('learning.overlays must be an object.');
|
|
1016
|
+
} else {
|
|
1017
|
+
pushStageError(errors, learning.overlays, 'learning.overlays');
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
800
1020
|
}
|
|
801
1021
|
}
|
|
802
1022
|
|
|
@@ -58,8 +58,12 @@ export function scanText(text, options = {}) {
|
|
|
58
58
|
if (gateEnabled === false) return clean;
|
|
59
59
|
if (typeof text !== 'string' || text.length === 0) return clean;
|
|
60
60
|
|
|
61
|
+
// sha256() emits lowercase hex; normalize caller/config digests so an
|
|
62
|
+
// uppercase-but-valid allowlist entry cannot silently fail to match.
|
|
61
63
|
const allowed = new Set(
|
|
62
|
-
Array.isArray(allowlistHashes)
|
|
64
|
+
Array.isArray(allowlistHashes)
|
|
65
|
+
? allowlistHashes.filter((h) => typeof h === 'string').map((h) => h.toLowerCase())
|
|
66
|
+
: [],
|
|
63
67
|
);
|
|
64
68
|
|
|
65
69
|
const labels = new Set();
|
|
@@ -114,5 +118,9 @@ export function loadSensitiveAllowlist(config) {
|
|
|
114
118
|
if (Array.isArray(security.sensitiveDataAllowlist)) {
|
|
115
119
|
candidates.push(...security.sensitiveDataAllowlist);
|
|
116
120
|
}
|
|
117
|
-
|
|
121
|
+
// Normalize to lowercase: sha256() emits lowercase hex, and an
|
|
122
|
+
// uppercase-but-valid config digest must still match (audit D1).
|
|
123
|
+
return candidates
|
|
124
|
+
.filter((v) => typeof v === 'string' && SHA256_HEX_RE.test(v))
|
|
125
|
+
.map((v) => v.toLowerCase());
|
|
118
126
|
}
|
|
@@ -116,24 +116,19 @@ function countTargetFiles(body) {
|
|
|
116
116
|
// separator row `|---|-----|...` are both skipped.
|
|
117
117
|
function countTestCases(body) {
|
|
118
118
|
if (body == null) return 0;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
sawHeader = true;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (!sawSeparator && /^\|[-\s|]+\|\s*$/.test(trimmed)) {
|
|
131
|
-
sawSeparator = true;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
count += 1;
|
|
119
|
+
const rows = body
|
|
120
|
+
.split('\n')
|
|
121
|
+
.map((line) => line.trim())
|
|
122
|
+
.filter((line) => line.startsWith('|'));
|
|
123
|
+
const separatorIdx = rows.findIndex((r) => /^\|[-\s|]+\|\s*$/.test(r));
|
|
124
|
+
if (separatorIdx >= 0) {
|
|
125
|
+
// Header row + separator row precede the data rows.
|
|
126
|
+
return rows.length - (separatorIdx + 1);
|
|
135
127
|
}
|
|
136
|
-
|
|
128
|
+
// No separator row: a leading canonical header (first cell '#') is still a
|
|
129
|
+
// header; every other pipe row is a test case — a headerless table counts
|
|
130
|
+
// every row, never silently dropping the first.
|
|
131
|
+
return rows[0] && /^\|\s*#\s*\|/.test(rows[0]) ? rows.length - 1 : rows.length;
|
|
137
132
|
}
|
|
138
133
|
|
|
139
134
|
// Extract the first ```bash ... ``` fence under `## Verification Commands` and split
|
|
@@ -66,24 +66,74 @@ function sliceSection(markdown, headingRe) {
|
|
|
66
66
|
return markdown.slice(start, end);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Target-file entries may carry brace alternation (`src/core/{a,b}.js`) or glob
|
|
70
|
+
// stars (`src/core/compact/*.js`) — planners write both. Expand them so a
|
|
71
|
+
// progress entry naming a covered file is not a false undeclared-drift.
|
|
72
|
+
function escapeRegExp(text) {
|
|
73
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function targetPatternToRegExp(pattern) {
|
|
77
|
+
// `{a,b}` → alternation; `**` → any path; `*` → any non-separator run.
|
|
78
|
+
let source = '';
|
|
79
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
80
|
+
const ch = pattern[i];
|
|
81
|
+
if (ch === '*') {
|
|
82
|
+
if (pattern[i + 1] === '*') {
|
|
83
|
+
source += '.*';
|
|
84
|
+
i += 1;
|
|
85
|
+
} else {
|
|
86
|
+
source += '[^/]*';
|
|
87
|
+
}
|
|
88
|
+
} else if (ch === '{') {
|
|
89
|
+
const close = pattern.indexOf('}', i + 1);
|
|
90
|
+
if (close === -1) {
|
|
91
|
+
source += '\\{';
|
|
92
|
+
} else {
|
|
93
|
+
source += `(?:${pattern.slice(i + 1, close).split(',').map(escapeRegExp).join('|')})`;
|
|
94
|
+
i = close;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
source += escapeRegExp(ch);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return new RegExp(`^${source}$`);
|
|
101
|
+
}
|
|
102
|
+
|
|
69
103
|
function collectTargetFiles(markdown) {
|
|
70
104
|
const section = sliceSection(markdown, TARGET_HEADING);
|
|
71
|
-
if (!section) return new Set();
|
|
72
105
|
const files = new Set();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
106
|
+
const patterns = [];
|
|
107
|
+
if (section) {
|
|
108
|
+
for (const line of section.split('\n')) {
|
|
109
|
+
// Lines look like "- `path/to/file.js` — what changes"
|
|
110
|
+
const m = /^\s*-\s+`?([^`\s]+(?:\.[^`\s]+)?)`?/.exec(line);
|
|
111
|
+
if (!m) continue;
|
|
112
|
+
const target = m[1];
|
|
113
|
+
if (/[{*]/.test(target)) patterns.push(targetPatternToRegExp(target));
|
|
114
|
+
else files.add(target);
|
|
115
|
+
}
|
|
77
116
|
}
|
|
78
|
-
return
|
|
117
|
+
return {
|
|
118
|
+
has(file) {
|
|
119
|
+
return files.has(file) || patterns.some((re) => re.test(file));
|
|
120
|
+
},
|
|
121
|
+
};
|
|
79
122
|
}
|
|
80
123
|
|
|
81
124
|
function collectProgressEntries(progressSection) {
|
|
82
125
|
const entries = [];
|
|
83
126
|
for (const line of progressSection.split('\n')) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
if (!trimmed.startsWith('- ')) continue;
|
|
129
|
+
const parsed = parseEntry(trimmed);
|
|
130
|
+
if (parsed) {
|
|
131
|
+
entries.push(parsed);
|
|
132
|
+
} else if (trimmed.includes('milestone:')) {
|
|
133
|
+
// A bullet that looks like a progress entry but fails the entry grammar is
|
|
134
|
+
// malformed — report it rather than silently dropping it.
|
|
135
|
+
entries.push({ ok: false, raw: trimmed });
|
|
136
|
+
}
|
|
87
137
|
}
|
|
88
138
|
return entries;
|
|
89
139
|
}
|