@worca/ui 0.28.0 → 0.30.0
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/app/main.bundle.js +1609 -1249
- package/app/main.bundle.js.map +4 -4
- package/app/styles.css +67 -4
- package/package.json +1 -1
- package/server/app.js +162 -2
- package/server/dispatch-defaults.js +16 -1
- package/server/dispatch-migration.js +180 -47
- package/server/graphify-status.js +234 -0
- package/server/settings-validator.js +59 -0
package/app/styles.css
CHANGED
|
@@ -1978,14 +1978,12 @@ sl-input [slot="prefix"] {
|
|
|
1978
1978
|
}
|
|
1979
1979
|
|
|
1980
1980
|
.dispatch-chip-locked {
|
|
1981
|
-
text-decoration: line-through;
|
|
1982
1981
|
opacity: 0.6;
|
|
1983
1982
|
}
|
|
1984
1983
|
|
|
1985
|
-
/* Auto-included meta-tool chips (Skill, Agent) — locked but
|
|
1986
|
-
* the
|
|
1984
|
+
/* Auto-included meta-tool chips (Skill, Agent) — locked but visually distinct
|
|
1985
|
+
* via the dashed border; present-and-required rather than blocked. */
|
|
1987
1986
|
.dispatch-chip-auto {
|
|
1988
|
-
text-decoration: none;
|
|
1989
1987
|
opacity: 0.75;
|
|
1990
1988
|
font-style: italic;
|
|
1991
1989
|
border-style: dashed;
|
|
@@ -2005,6 +2003,16 @@ sl-input [slot="prefix"] {
|
|
|
2005
2003
|
font-size: 10px;
|
|
2006
2004
|
}
|
|
2007
2005
|
|
|
2006
|
+
/* Inherits-defaults placeholder — shown when per-agent entry is an empty
|
|
2007
|
+
* list, which the resolver treats as fall-through to _defaults. Visually
|
|
2008
|
+
* lighter than lockdown so the two states are easy to distinguish. */
|
|
2009
|
+
.dispatch-chip-inherits {
|
|
2010
|
+
text-decoration: none;
|
|
2011
|
+
opacity: 0.6;
|
|
2012
|
+
font-style: italic;
|
|
2013
|
+
font-size: 10px;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2008
2016
|
.dispatch-chip-warn {
|
|
2009
2017
|
--sl-color-neutral-200: var(--sl-color-warning-200);
|
|
2010
2018
|
background: var(--sl-color-warning-100);
|
|
@@ -2015,6 +2023,21 @@ sl-input [slot="prefix"] {
|
|
|
2015
2023
|
margin-bottom: 16px;
|
|
2016
2024
|
}
|
|
2017
2025
|
|
|
2026
|
+
/* Header row: optional section title on the left, per-section Reset on the
|
|
2027
|
+
* right. Rendered at the top of each dispatch panel body (visible only when the
|
|
2028
|
+
* sl-details is expanded). */
|
|
2029
|
+
.dispatch-section-header {
|
|
2030
|
+
display: flex;
|
|
2031
|
+
align-items: center;
|
|
2032
|
+
justify-content: space-between;
|
|
2033
|
+
gap: 8px;
|
|
2034
|
+
min-height: 28px;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
.dispatch-section-reset {
|
|
2038
|
+
margin-left: auto;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2018
2041
|
.dispatch-section-title {
|
|
2019
2042
|
font-size: 14px;
|
|
2020
2043
|
font-weight: 600;
|
|
@@ -6450,3 +6473,43 @@ sl-dialog.markdown-dialog::part(body) {
|
|
|
6450
6473
|
.conflict-icon {
|
|
6451
6474
|
color: var(--status-blocked);
|
|
6452
6475
|
}
|
|
6476
|
+
|
|
6477
|
+
/* Graphify cache location — selectable monospace path (W-053 cache relocation) */
|
|
6478
|
+
.graphify-codebox {
|
|
6479
|
+
display: block;
|
|
6480
|
+
margin: 0.25rem 0 0.75rem;
|
|
6481
|
+
padding: 0.4rem 0.6rem;
|
|
6482
|
+
background: var(--bg-secondary);
|
|
6483
|
+
border: 1px solid var(--border-subtle);
|
|
6484
|
+
border-radius: var(--radius);
|
|
6485
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6486
|
+
font-size: 0.85rem;
|
|
6487
|
+
color: var(--fg);
|
|
6488
|
+
word-break: break-all;
|
|
6489
|
+
user-select: all;
|
|
6490
|
+
}
|
|
6491
|
+
|
|
6492
|
+
/* A monospace value/command box with a copy button pinned to the right. */
|
|
6493
|
+
.graphify-copy-row {
|
|
6494
|
+
display: flex;
|
|
6495
|
+
align-items: center;
|
|
6496
|
+
gap: 0.4rem;
|
|
6497
|
+
margin: 0.25rem 0 0.75rem;
|
|
6498
|
+
}
|
|
6499
|
+
|
|
6500
|
+
.graphify-copy-row .graphify-codebox {
|
|
6501
|
+
flex: 1;
|
|
6502
|
+
min-width: 0;
|
|
6503
|
+
margin: 0;
|
|
6504
|
+
}
|
|
6505
|
+
|
|
6506
|
+
/* Shown when the graphify CLI is missing/incompatible. */
|
|
6507
|
+
.graphify-not-installed {
|
|
6508
|
+
margin: 0 0 0.75rem;
|
|
6509
|
+
}
|
|
6510
|
+
|
|
6511
|
+
/* Caution color (action needed: install) on the message text only — not the
|
|
6512
|
+
install-command code box, which keeps normal monospace styling. */
|
|
6513
|
+
.graphify-not-installed .settings-tab-description {
|
|
6514
|
+
color: var(--status-paused);
|
|
6515
|
+
}
|
package/package.json
CHANGED
package/server/app.js
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { execFile, execFileSync, spawn } from 'node:child_process';
|
|
4
4
|
import { createHmac, randomUUID } from 'node:crypto';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
6
12
|
import { homedir } from 'node:os';
|
|
7
13
|
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
8
14
|
import { fileURLToPath } from 'node:url';
|
|
@@ -10,13 +16,19 @@ import express from 'express';
|
|
|
10
16
|
|
|
11
17
|
import { dbExists, getIssue, listIssues } from './beads-reader.js';
|
|
12
18
|
import { createFleetRouter } from './fleet-routes.js';
|
|
19
|
+
import {
|
|
20
|
+
_effectiveConfig,
|
|
21
|
+
clearRepoCache,
|
|
22
|
+
createGraphifyStatus,
|
|
23
|
+
snapshotDir,
|
|
24
|
+
} from './graphify-status.js';
|
|
13
25
|
import { RAW_BODY } from './integrations/index.js';
|
|
14
26
|
import { verify } from './integrations/verify.js';
|
|
15
27
|
import { LaunchLock } from './launch-lock.js';
|
|
16
28
|
import { fleetRunsDir, workspaceRunsDir, workspacesDir } from './paths.js';
|
|
17
29
|
import { createPreferencesRouter } from './preferences-routes.js';
|
|
18
30
|
import { ProcessManager } from './process-manager.js';
|
|
19
|
-
import { scanDirectory } from './project-registry.js';
|
|
31
|
+
import { readProjects, scanDirectory } from './project-registry.js';
|
|
20
32
|
import {
|
|
21
33
|
createProjectRoutes,
|
|
22
34
|
createProjectScopedRoutes,
|
|
@@ -994,6 +1006,154 @@ export function createApp(options = {}) {
|
|
|
994
1006
|
res.json({ ok: true, path: configPath });
|
|
995
1007
|
});
|
|
996
1008
|
|
|
1009
|
+
// ─── Graphify endpoints ──────────────────────────────────────────────
|
|
1010
|
+
if (!app.locals.graphifyStatus) {
|
|
1011
|
+
app.locals.graphifyStatus = createGraphifyStatus({});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Resolve the graphify settings for a request. In single-project mode the
|
|
1015
|
+
// server's own projectRoot/settingsPath are used. In global mode there is no
|
|
1016
|
+
// fixed project, so the selected project is passed as ?project=<id> and we
|
|
1017
|
+
// resolve its root + settings.json from the registry — otherwise the endpoint
|
|
1018
|
+
// would be blind to every project and only ever see global settings.
|
|
1019
|
+
function readGraphifySettings(projectId) {
|
|
1020
|
+
const readJson = (p) => {
|
|
1021
|
+
if (!p) return {};
|
|
1022
|
+
try {
|
|
1023
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
1024
|
+
} catch {
|
|
1025
|
+
return {};
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
const globalSettingsPath = prefsDir
|
|
1029
|
+
? join(prefsDir, 'settings.json')
|
|
1030
|
+
: settingsPath;
|
|
1031
|
+
|
|
1032
|
+
let projectSettingsPath = settingsPath;
|
|
1033
|
+
let root = projectRoot || process.cwd();
|
|
1034
|
+
if (projectId && prefsDir) {
|
|
1035
|
+
const proj = readProjects(prefsDir).find((p) => p.name === projectId);
|
|
1036
|
+
if (proj) {
|
|
1037
|
+
projectSettingsPath =
|
|
1038
|
+
proj.settingsPath || join(proj.path, '.claude', 'settings.json');
|
|
1039
|
+
root = proj.path;
|
|
1040
|
+
}
|
|
1041
|
+
} else if (!projectSettingsPath && projectRoot) {
|
|
1042
|
+
projectSettingsPath = join(projectRoot, '.claude', 'settings.json');
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
globalSettings: readJson(globalSettingsPath),
|
|
1047
|
+
projectSettings: readJson(projectSettingsPath),
|
|
1048
|
+
root,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function graphifyStatusPayload(projectId) {
|
|
1053
|
+
const { globalSettings, projectSettings, root } =
|
|
1054
|
+
readGraphifySettings(projectId);
|
|
1055
|
+
const result = await app.locals.graphifyStatus.getStatus({
|
|
1056
|
+
globalSettings,
|
|
1057
|
+
projectSettings,
|
|
1058
|
+
projectRoot: root,
|
|
1059
|
+
});
|
|
1060
|
+
return { ...result, building: Boolean(app.locals.graphifyBuilding) };
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
app.get('/api/graphify/status', async (req, res) => {
|
|
1064
|
+
try {
|
|
1065
|
+
res.json(await graphifyStatusPayload(req.query.project));
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
app.post('/api/graphify/recheck', async (req, res) => {
|
|
1072
|
+
try {
|
|
1073
|
+
app.locals.graphifyStatus.invalidate();
|
|
1074
|
+
res.json(await graphifyStatusPayload(req.query.project));
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// Build/refresh the current HEAD's cache snapshot (async, long-running).
|
|
1081
|
+
// The lock + .complete publish discipline lives in run_graphify_preflight,
|
|
1082
|
+
// so we drive it via a detached Python process and track a `building` flag
|
|
1083
|
+
// the UI polls through /api/graphify/status.
|
|
1084
|
+
app.post('/api/graphify/build', async (req, res) => {
|
|
1085
|
+
try {
|
|
1086
|
+
const { globalSettings, projectSettings, root } = readGraphifySettings(
|
|
1087
|
+
req.query.project,
|
|
1088
|
+
);
|
|
1089
|
+
const effective = _effectiveConfig(globalSettings, projectSettings);
|
|
1090
|
+
if (!effective.enabled) {
|
|
1091
|
+
return res
|
|
1092
|
+
.status(400)
|
|
1093
|
+
.json({ ok: false, error: 'Graphify is not enabled' });
|
|
1094
|
+
}
|
|
1095
|
+
const detection = await app.locals.graphifyStatus.detect();
|
|
1096
|
+
if (!detection.installed || !detection.compatible) {
|
|
1097
|
+
return res.status(400).json({
|
|
1098
|
+
ok: false,
|
|
1099
|
+
error:
|
|
1100
|
+
detection.error || 'Graphify is not installed or not compatible',
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
if (app.locals.graphifyBuilding) {
|
|
1104
|
+
return res.json({ ok: true, status: 'building' });
|
|
1105
|
+
}
|
|
1106
|
+
// Fresh build for the current HEAD: clear its snapshot first.
|
|
1107
|
+
const snap = snapshotDir(root);
|
|
1108
|
+
if (snap) {
|
|
1109
|
+
try {
|
|
1110
|
+
rmSync(snap, { recursive: true, force: true });
|
|
1111
|
+
} catch {}
|
|
1112
|
+
}
|
|
1113
|
+
app.locals.graphifyBuilding = true;
|
|
1114
|
+
const child = spawn(
|
|
1115
|
+
'python3',
|
|
1116
|
+
[
|
|
1117
|
+
'-c',
|
|
1118
|
+
'from worca.scripts.graphify_preflight import run_graphify_preflight as r; r()',
|
|
1119
|
+
],
|
|
1120
|
+
{ cwd: root, stdio: 'ignore' },
|
|
1121
|
+
);
|
|
1122
|
+
const done = () => {
|
|
1123
|
+
app.locals.graphifyBuilding = false;
|
|
1124
|
+
app.locals.graphifyStatus.invalidate();
|
|
1125
|
+
};
|
|
1126
|
+
child.on('exit', done);
|
|
1127
|
+
child.on('error', done);
|
|
1128
|
+
res.json({ ok: true, status: 'building' });
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
app.locals.graphifyBuilding = false;
|
|
1131
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// Clear ALL cached snapshots for this project's repo.
|
|
1136
|
+
app.post('/api/graphify/clear', async (req, res) => {
|
|
1137
|
+
try {
|
|
1138
|
+
const { root } = readGraphifySettings(req.query.project);
|
|
1139
|
+
const cleared = clearRepoCache(root);
|
|
1140
|
+
app.locals.graphifyStatus.invalidate();
|
|
1141
|
+
res.json({ ok: true, cleared });
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
app.get('/api/graphify/graph.html', (req, res) => {
|
|
1148
|
+
const { root } = readGraphifySettings(req.query.project);
|
|
1149
|
+
const snap = snapshotDir(root);
|
|
1150
|
+
const htmlPath = snap ? join(snap, 'graphify', 'graph.html') : null;
|
|
1151
|
+
if (!htmlPath || !existsSync(htmlPath)) {
|
|
1152
|
+
return res.status(404).json({ ok: false, error: 'graph.html not found' });
|
|
1153
|
+
}
|
|
1154
|
+
res.sendFile(htmlPath);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
997
1157
|
// ─── Dynamic favicon ──────────────────────────────────────────────────
|
|
998
1158
|
// Serve mode-specific favicon before express.static so it takes precedence.
|
|
999
1159
|
app.get('/favicon.svg', (_req, res) => {
|
|
@@ -18,7 +18,22 @@ export const DISPATCH_DEFAULTS = {
|
|
|
18
18
|
'fewer-permission-prompts',
|
|
19
19
|
'loop',
|
|
20
20
|
'schedule',
|
|
21
|
-
|
|
21
|
+
// worca-* dev skills that genuinely must stay off-limits to pipeline
|
|
22
|
+
// agents: release/publish, PR merges, cross-repo sync, installation,
|
|
23
|
+
// agent/governance override (privilege escalation), pipeline launch
|
|
24
|
+
// (recursion), and autonomous issue/plan creation. The rest of the
|
|
25
|
+
// worca-* dev tooling (precommit, coverage, ui/event scaffolding,
|
|
26
|
+
// webhook-test, issue read) is allowed via the per-agent '*' wildcard.
|
|
27
|
+
'worca-release',
|
|
28
|
+
'worca-rc',
|
|
29
|
+
'worca-pr-prep',
|
|
30
|
+
'worca-install',
|
|
31
|
+
'worca-sync',
|
|
32
|
+
'worca-sync-commit',
|
|
33
|
+
'worca-sync-pr',
|
|
34
|
+
'worca-agent-override',
|
|
35
|
+
'worca-analyze',
|
|
36
|
+
'worca-plan-new',
|
|
22
37
|
'update-config',
|
|
23
38
|
'hookify:hookify',
|
|
24
39
|
'hookify:configure',
|
|
@@ -55,12 +55,141 @@ function _absorbFlatDispatchKeys(dispatch) {
|
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// --- One-time dispatch-default normalization (W-054 follow-up) -------------
|
|
59
|
+
//
|
|
60
|
+
// Mirror of normalize_dispatch_defaults() in src/worca/hooks/tracking.py.
|
|
61
|
+
// Bumped when a new one-time normalization is added; stamped onto
|
|
62
|
+
// governance.dispatch_migration_version so it runs exactly once per config.
|
|
63
|
+
export const DISPATCH_MIGRATION_VERSION = 1;
|
|
64
|
+
|
|
65
|
+
// Pre-W-054 (W-038-era) shipped subagent default: every pipeline agent capped
|
|
66
|
+
// to Explore-only. coordinator:[] / empty lists fall through to _defaults and
|
|
67
|
+
// are ignored in the comparison.
|
|
68
|
+
const _LEGACY_EXPLORE_SUBAGENT_DEFAULT = {
|
|
69
|
+
planner: ['Explore'],
|
|
70
|
+
implementer: ['Explore'],
|
|
71
|
+
tester: ['Explore'],
|
|
72
|
+
guardian: ['Explore'],
|
|
73
|
+
reviewer: ['Explore'],
|
|
74
|
+
plan_reviewer: ['Explore'],
|
|
75
|
+
learner: ['Explore'],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Pre-narrowing skills denylist (carried the broad `worca-*` glob).
|
|
79
|
+
const _LEGACY_SKILLS_ALWAYS_DISALLOWED = new Set([
|
|
80
|
+
'batch',
|
|
81
|
+
'fewer-permission-prompts',
|
|
82
|
+
'loop',
|
|
83
|
+
'schedule',
|
|
84
|
+
'worca-*',
|
|
85
|
+
'update-config',
|
|
86
|
+
'hookify:hookify',
|
|
87
|
+
'hookify:configure',
|
|
88
|
+
'hookify:list',
|
|
89
|
+
'hookify:writing-rules',
|
|
90
|
+
'init',
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
function _canonicalPerAgent(perAgent) {
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const [agent, allow] of Object.entries(perAgent)) {
|
|
96
|
+
if (agent === '_defaults') continue;
|
|
97
|
+
if (!Array.isArray(allow) || allow.length === 0) continue;
|
|
98
|
+
out[agent] = [...allow].sort();
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _sameStringMap(a, b) {
|
|
104
|
+
const ak = Object.keys(a);
|
|
105
|
+
const bk = Object.keys(b);
|
|
106
|
+
if (ak.length !== bk.length) return false;
|
|
107
|
+
for (const k of ak) {
|
|
108
|
+
const av = a[k];
|
|
109
|
+
const bv = b[k];
|
|
110
|
+
if (!Array.isArray(bv) || av.length !== bv.length) return false;
|
|
111
|
+
for (let i = 0; i < av.length; i++) {
|
|
112
|
+
if (av[i] !== bv[i]) return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Collapse a stale Explore-only per_agent_allow to the new `_defaults: ["*"]`
|
|
120
|
+
* default. Only fires on the untouched W-038 shape with an unset/wildcard
|
|
121
|
+
* _defaults. Returns true if changed.
|
|
122
|
+
*/
|
|
123
|
+
export function adoptStaleSubagentDefault(subagentsCfg) {
|
|
124
|
+
if (!subagentsCfg || typeof subagentsCfg !== 'object') return false;
|
|
125
|
+
const pa = subagentsCfg.per_agent_allow;
|
|
126
|
+
if (!pa || typeof pa !== 'object' || Array.isArray(pa)) return false;
|
|
127
|
+
const def = pa._defaults;
|
|
128
|
+
const defOk =
|
|
129
|
+
def === undefined ||
|
|
130
|
+
(Array.isArray(def) && def.length === 1 && def[0] === '*');
|
|
131
|
+
if (!defOk) return false;
|
|
132
|
+
const expected = _canonicalPerAgent(_LEGACY_EXPLORE_SUBAGENT_DEFAULT);
|
|
133
|
+
if (!_sameStringMap(_canonicalPerAgent(pa), expected)) return false;
|
|
134
|
+
subagentsCfg.per_agent_allow = { _defaults: ['*'] };
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Widen an untouched skills denylist (broad `worca-*`) to the current set.
|
|
140
|
+
* Exact-match (set) guarded. Returns true if changed.
|
|
141
|
+
*/
|
|
142
|
+
export function adoptNarrowedSkillsDenylist(skillsCfg) {
|
|
143
|
+
if (!skillsCfg || typeof skillsCfg !== 'object') return false;
|
|
144
|
+
const current = skillsCfg.always_disallowed;
|
|
145
|
+
if (!Array.isArray(current)) return false;
|
|
146
|
+
if (current.length !== _LEGACY_SKILLS_ALWAYS_DISALLOWED.size) return false;
|
|
147
|
+
for (const item of current) {
|
|
148
|
+
if (!_LEGACY_SKILLS_ALWAYS_DISALLOWED.has(item)) return false;
|
|
149
|
+
}
|
|
150
|
+
skillsCfg.always_disallowed = [...DISPATCH_DEFAULTS.skills.always_disallowed];
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Apply one-time dispatch-default normalizations, gated by a version stamp.
|
|
156
|
+
* Brings an *untouched* config up to current shipped defaults for the two
|
|
157
|
+
* things that changed after W-054 (subagent per_agent_allow, skills denylist).
|
|
158
|
+
* Mutates governanceCfg; returns change descriptions.
|
|
159
|
+
*/
|
|
160
|
+
export function normalizeDispatchDefaults(governanceCfg) {
|
|
161
|
+
const changes = [];
|
|
162
|
+
if (!governanceCfg || typeof governanceCfg !== 'object') return changes;
|
|
163
|
+
let stamp = governanceCfg.dispatch_migration_version;
|
|
164
|
+
if (!Number.isInteger(stamp)) stamp = 0;
|
|
165
|
+
if (stamp >= DISPATCH_MIGRATION_VERSION) return changes;
|
|
166
|
+
const dispatch = governanceCfg.dispatch;
|
|
167
|
+
if (!dispatch || typeof dispatch !== 'object' || Array.isArray(dispatch)) {
|
|
168
|
+
return changes;
|
|
169
|
+
}
|
|
170
|
+
if (adoptStaleSubagentDefault(dispatch.subagents)) {
|
|
171
|
+
changes.push(
|
|
172
|
+
'governance.dispatch.subagents: adopted new default (_defaults:["*"]) for config pinned to legacy Explore-only set',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (adoptNarrowedSkillsDenylist(dispatch.skills)) {
|
|
176
|
+
changes.push(
|
|
177
|
+
'governance.dispatch.skills.always_disallowed: narrowed legacy "worca-*" glob to the current must-disallow set',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
governanceCfg.dispatch_migration_version = DISPATCH_MIGRATION_VERSION;
|
|
181
|
+
return changes;
|
|
182
|
+
}
|
|
183
|
+
|
|
58
184
|
/**
|
|
59
185
|
* Migrate legacy governance.subagent_dispatch and/or legacy flat
|
|
60
|
-
* governance.dispatch (agent-keyed) → governance.dispatch.subagents.per_agent_allow
|
|
186
|
+
* governance.dispatch (agent-keyed) → governance.dispatch.subagents.per_agent_allow,
|
|
187
|
+
* then apply the one-time dispatch-default normalization.
|
|
61
188
|
*
|
|
62
|
-
* Seeds _defaults, adds tools/skills defaults, drops _dispatch_legacy.
|
|
63
|
-
*
|
|
189
|
+
* Seeds _defaults, adds tools/skills defaults, drops _dispatch_legacy. The
|
|
190
|
+
* normalization runs even with no legacy shape so already-migrated configs
|
|
191
|
+
* pinned to the stale Explore-only subagent default (or the broad `worca-*`
|
|
192
|
+
* skills glob) self-heal on next save. Gated by a version stamp → idempotent.
|
|
64
193
|
*
|
|
65
194
|
* @param {object} worcaConfig — the `worca` object from settings (mutated)
|
|
66
195
|
* @returns {string[]} list of change descriptions (empty = no-op)
|
|
@@ -73,60 +202,64 @@ export function migrateDispatchGovernance(worcaConfig) {
|
|
|
73
202
|
const hasSubagentDispatch = 'subagent_dispatch' in gov;
|
|
74
203
|
const hasLegacyFlatDispatch = _isLegacyFlatDispatch(gov.dispatch);
|
|
75
204
|
|
|
76
|
-
if (
|
|
205
|
+
if (hasSubagentDispatch || hasLegacyFlatDispatch) {
|
|
206
|
+
if (!gov.dispatch || Array.isArray(gov.dispatch)) gov.dispatch = {};
|
|
207
|
+
const dispatch = gov.dispatch;
|
|
77
208
|
|
|
78
|
-
|
|
79
|
-
|
|
209
|
+
// Absorb legacy flat shape (pre-W-038) first so subagent_dispatch values
|
|
210
|
+
// take precedence below.
|
|
211
|
+
if (hasLegacyFlatDispatch) {
|
|
212
|
+
_absorbFlatDispatchKeys(dispatch);
|
|
213
|
+
changes.push(
|
|
214
|
+
'governance.dispatch (flat agent-keyed) -> governance.dispatch.subagents (W-054)',
|
|
215
|
+
);
|
|
216
|
+
}
|
|
80
217
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
218
|
+
if (hasSubagentDispatch) {
|
|
219
|
+
const old = gov.subagent_dispatch;
|
|
220
|
+
delete gov.subagent_dispatch;
|
|
221
|
+
if (!dispatch.subagents) dispatch.subagents = {};
|
|
222
|
+
if (!dispatch.subagents.per_agent_allow) {
|
|
223
|
+
dispatch.subagents.per_agent_allow = {};
|
|
224
|
+
}
|
|
225
|
+
Object.assign(dispatch.subagents.per_agent_allow, old);
|
|
226
|
+
changes.push(
|
|
227
|
+
'governance.subagent_dispatch -> governance.dispatch.subagents (W-054)',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
89
230
|
|
|
90
|
-
if (hasSubagentDispatch) {
|
|
91
|
-
const old = gov.subagent_dispatch;
|
|
92
|
-
delete gov.subagent_dispatch;
|
|
93
231
|
if (!dispatch.subagents) dispatch.subagents = {};
|
|
94
|
-
|
|
95
|
-
dispatch.subagents.per_agent_allow = {};
|
|
96
|
-
}
|
|
97
|
-
Object.assign(dispatch.subagents.per_agent_allow, old);
|
|
98
|
-
changes.push(
|
|
99
|
-
'governance.subagent_dispatch -> governance.dispatch.subagents (W-054)',
|
|
100
|
-
);
|
|
101
|
-
}
|
|
232
|
+
const subagents = dispatch.subagents;
|
|
102
233
|
|
|
103
|
-
|
|
104
|
-
|
|
234
|
+
if (!subagents.per_agent_allow) subagents.per_agent_allow = {};
|
|
235
|
+
if (!('_defaults' in subagents.per_agent_allow)) {
|
|
236
|
+
subagents.per_agent_allow._defaults = [
|
|
237
|
+
...DISPATCH_DEFAULTS.subagents.per_agent_allow._defaults,
|
|
238
|
+
];
|
|
239
|
+
}
|
|
105
240
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
241
|
+
if (!subagents.always_disallowed) {
|
|
242
|
+
subagents.always_disallowed = [
|
|
243
|
+
...DISPATCH_DEFAULTS.subagents.always_disallowed,
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
if (!subagents.default_denied) {
|
|
247
|
+
subagents.default_denied = [
|
|
248
|
+
...DISPATCH_DEFAULTS.subagents.default_denied,
|
|
249
|
+
];
|
|
250
|
+
}
|
|
112
251
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
subagents.default_denied = [...DISPATCH_DEFAULTS.subagents.default_denied];
|
|
120
|
-
}
|
|
252
|
+
if (!dispatch.tools) {
|
|
253
|
+
dispatch.tools = structuredClone(DISPATCH_DEFAULTS.tools);
|
|
254
|
+
}
|
|
255
|
+
if (!dispatch.skills) {
|
|
256
|
+
dispatch.skills = structuredClone(DISPATCH_DEFAULTS.skills);
|
|
257
|
+
}
|
|
121
258
|
|
|
122
|
-
|
|
123
|
-
dispatch.tools = structuredClone(DISPATCH_DEFAULTS.tools);
|
|
124
|
-
}
|
|
125
|
-
if (!dispatch.skills) {
|
|
126
|
-
dispatch.skills = structuredClone(DISPATCH_DEFAULTS.skills);
|
|
259
|
+
delete gov._dispatch_legacy;
|
|
127
260
|
}
|
|
128
261
|
|
|
129
|
-
|
|
262
|
+
changes.push(...normalizeDispatchDefaults(gov));
|
|
130
263
|
|
|
131
264
|
return changes;
|
|
132
265
|
}
|