@worca/ui 0.26.0 → 0.28.0-rc.1
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 +1686 -1539
- package/app/main.bundle.js.map +4 -4
- package/app/styles.css +281 -1
- package/package.json +3 -1
- package/server/app.js +47 -1
- package/server/beads-reader.js +9 -2
- package/server/dispatch-defaults.js +81 -0
- package/server/dispatch-events-aggregator.js +25 -19
- package/server/dispatch-migration.js +132 -0
- package/server/git-helpers.js +32 -0
- package/server/known-skills.json +31 -0
- package/server/known-tools.json +18 -0
- package/server/model-env-routes.js +7 -2
- package/server/project-routes.js +54 -16
- package/server/settings-validator.js +98 -13
- package/server/watcher.js +2 -0
- package/server/worktrees-routes.js +4 -0
- package/server/ws-message-router.js +26 -8
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch governance migration — JS port of Python
|
|
3
|
+
* _migrate_dispatch_governance() in src/worca/cli/init.py (§10.3).
|
|
4
|
+
*
|
|
5
|
+
* Mutates worcaConfig in place following the same pattern as
|
|
6
|
+
* global-keys.js:extractAndStripGlobalKeys().
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { DISPATCH_DEFAULTS } from './dispatch-defaults.js';
|
|
10
|
+
|
|
11
|
+
const _DISPATCH_SECTION_KEYS = new Set(['tools', 'skills', 'subagents']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns true if `dispatch` has at least one agent-name key directly at the
|
|
15
|
+
* top level (the pre-W-038 legacy flat shape, e.g. `{planner: [...]}`),
|
|
16
|
+
* rather than the W-054 nested shape `{tools, skills, subagents}`.
|
|
17
|
+
*/
|
|
18
|
+
function _isLegacyFlatDispatch(dispatch) {
|
|
19
|
+
if (!dispatch || typeof dispatch !== 'object' || Array.isArray(dispatch)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
for (const key of Object.keys(dispatch)) {
|
|
23
|
+
if (!_DISPATCH_SECTION_KEYS.has(key) && Array.isArray(dispatch[key])) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Strip agent-name keys from `dispatch`, moving their values into
|
|
32
|
+
* `dispatch.subagents.per_agent_allow` so the legacy flat shape is normalized.
|
|
33
|
+
*/
|
|
34
|
+
function _absorbFlatDispatchKeys(dispatch) {
|
|
35
|
+
const flatKeys = [];
|
|
36
|
+
for (const key of Object.keys(dispatch)) {
|
|
37
|
+
if (!_DISPATCH_SECTION_KEYS.has(key) && Array.isArray(dispatch[key])) {
|
|
38
|
+
flatKeys.push(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (flatKeys.length === 0) return false;
|
|
42
|
+
|
|
43
|
+
if (!dispatch.subagents) dispatch.subagents = {};
|
|
44
|
+
if (!dispatch.subagents.per_agent_allow) {
|
|
45
|
+
dispatch.subagents.per_agent_allow = {};
|
|
46
|
+
}
|
|
47
|
+
for (const key of flatKeys) {
|
|
48
|
+
const incoming = dispatch[key];
|
|
49
|
+
const existing = dispatch.subagents.per_agent_allow[key];
|
|
50
|
+
if (!existing || existing.length === 0) {
|
|
51
|
+
dispatch.subagents.per_agent_allow[key] = incoming;
|
|
52
|
+
}
|
|
53
|
+
delete dispatch[key];
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Migrate legacy governance.subagent_dispatch and/or legacy flat
|
|
60
|
+
* governance.dispatch (agent-keyed) → governance.dispatch.subagents.per_agent_allow.
|
|
61
|
+
*
|
|
62
|
+
* Seeds _defaults, adds tools/skills defaults, drops _dispatch_legacy.
|
|
63
|
+
* Idempotent — returns [] on already-migrated configs.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} worcaConfig — the `worca` object from settings (mutated)
|
|
66
|
+
* @returns {string[]} list of change descriptions (empty = no-op)
|
|
67
|
+
*/
|
|
68
|
+
export function migrateDispatchGovernance(worcaConfig) {
|
|
69
|
+
const changes = [];
|
|
70
|
+
const gov = worcaConfig.governance;
|
|
71
|
+
if (!gov || typeof gov !== 'object') return changes;
|
|
72
|
+
|
|
73
|
+
const hasSubagentDispatch = 'subagent_dispatch' in gov;
|
|
74
|
+
const hasLegacyFlatDispatch = _isLegacyFlatDispatch(gov.dispatch);
|
|
75
|
+
|
|
76
|
+
if (!hasSubagentDispatch && !hasLegacyFlatDispatch) return changes;
|
|
77
|
+
|
|
78
|
+
if (!gov.dispatch || Array.isArray(gov.dispatch)) gov.dispatch = {};
|
|
79
|
+
const dispatch = gov.dispatch;
|
|
80
|
+
|
|
81
|
+
// Absorb legacy flat shape (pre-W-038) first so subagent_dispatch values
|
|
82
|
+
// take precedence below.
|
|
83
|
+
if (hasLegacyFlatDispatch) {
|
|
84
|
+
_absorbFlatDispatchKeys(dispatch);
|
|
85
|
+
changes.push(
|
|
86
|
+
'governance.dispatch (flat agent-keyed) -> governance.dispatch.subagents (W-054)',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (hasSubagentDispatch) {
|
|
91
|
+
const old = gov.subagent_dispatch;
|
|
92
|
+
delete gov.subagent_dispatch;
|
|
93
|
+
if (!dispatch.subagents) dispatch.subagents = {};
|
|
94
|
+
if (!dispatch.subagents.per_agent_allow) {
|
|
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
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!dispatch.subagents) dispatch.subagents = {};
|
|
104
|
+
const subagents = dispatch.subagents;
|
|
105
|
+
|
|
106
|
+
if (!subagents.per_agent_allow) subagents.per_agent_allow = {};
|
|
107
|
+
if (!('_defaults' in subagents.per_agent_allow)) {
|
|
108
|
+
subagents.per_agent_allow._defaults = [
|
|
109
|
+
...DISPATCH_DEFAULTS.subagents.per_agent_allow._defaults,
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!subagents.always_disallowed) {
|
|
114
|
+
subagents.always_disallowed = [
|
|
115
|
+
...DISPATCH_DEFAULTS.subagents.always_disallowed,
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
if (!subagents.default_denied) {
|
|
119
|
+
subagents.default_denied = [...DISPATCH_DEFAULTS.subagents.default_denied];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!dispatch.tools) {
|
|
123
|
+
dispatch.tools = structuredClone(DISPATCH_DEFAULTS.tools);
|
|
124
|
+
}
|
|
125
|
+
if (!dispatch.skills) {
|
|
126
|
+
dispatch.skills = structuredClone(DISPATCH_DEFAULTS.skills);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
delete gov._dispatch_legacy;
|
|
130
|
+
|
|
131
|
+
return changes;
|
|
132
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const CACHE_TTL_MS = 30_000;
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
|
|
6
|
+
function _resolveDefaultBranch(projectRoot) {
|
|
7
|
+
try {
|
|
8
|
+
const out = execFileSync(
|
|
9
|
+
'git',
|
|
10
|
+
['symbolic-ref', 'refs/remotes/origin/HEAD', '--short'],
|
|
11
|
+
{ cwd: projectRoot, encoding: 'utf8', timeout: 5000 },
|
|
12
|
+
);
|
|
13
|
+
const branch = out.trim().replace(/^origin\//, '');
|
|
14
|
+
if (branch) return branch;
|
|
15
|
+
} catch {
|
|
16
|
+
// no symbolic-ref configured — fall through
|
|
17
|
+
}
|
|
18
|
+
return 'main';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getDefaultBranch(projectRoot) {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const hit = cache.get(projectRoot);
|
|
24
|
+
if (hit && hit.expiresAt > now) return hit.value;
|
|
25
|
+
const value = _resolveDefaultBranch(projectRoot);
|
|
26
|
+
cache.set(projectRoot, { value, expiresAt: now + CACHE_TTL_MS });
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function _clearDefaultBranchCache() {
|
|
31
|
+
cache.clear();
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "batch", "group": "Built-in" },
|
|
3
|
+
{ "name": "claude-api", "group": "Built-in" },
|
|
4
|
+
{ "name": "debug", "group": "Built-in" },
|
|
5
|
+
{ "name": "fewer-permission-prompts", "group": "Built-in" },
|
|
6
|
+
{ "name": "init", "group": "Built-in" },
|
|
7
|
+
{ "name": "loop", "group": "Built-in" },
|
|
8
|
+
{ "name": "review", "group": "Built-in" },
|
|
9
|
+
{ "name": "schedule", "group": "Built-in" },
|
|
10
|
+
{ "name": "security-review", "group": "Built-in" },
|
|
11
|
+
{ "name": "simplify", "group": "Built-in" },
|
|
12
|
+
{ "name": "update-config", "group": "Built-in" },
|
|
13
|
+
{ "name": "hookify:hookify", "group": "Plugin" },
|
|
14
|
+
{ "name": "hookify:configure", "group": "Plugin" },
|
|
15
|
+
{ "name": "hookify:list", "group": "Plugin" },
|
|
16
|
+
{ "name": "hookify:writing-rules", "group": "Plugin" },
|
|
17
|
+
{ "name": "feature-dev:feature-dev", "group": "Plugin" },
|
|
18
|
+
{ "name": "feature-dev:code-reviewer", "group": "Plugin" },
|
|
19
|
+
{ "name": "feature-dev:code-architect", "group": "Plugin" },
|
|
20
|
+
{ "name": "feature-dev:code-explorer", "group": "Plugin" },
|
|
21
|
+
{ "name": "claude-md-management:revise-claude-md", "group": "Plugin" },
|
|
22
|
+
{ "name": "claude-md-management:claude-md-improver", "group": "Plugin" },
|
|
23
|
+
{ "name": "worca-install", "group": "Worca" },
|
|
24
|
+
{ "name": "worca-rc", "group": "Worca" },
|
|
25
|
+
{ "name": "worca-release", "group": "Worca" },
|
|
26
|
+
{ "name": "worca-sync", "group": "Worca" },
|
|
27
|
+
{ "name": "worca-sync-pr", "group": "Worca" },
|
|
28
|
+
{ "name": "worca-sync-commit", "group": "Worca" },
|
|
29
|
+
{ "name": "worca-analyze", "group": "Worca" },
|
|
30
|
+
{ "name": "worca-agent-override", "group": "Worca" }
|
|
31
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "Agent", "group": "Core" },
|
|
3
|
+
{ "name": "Bash", "group": "Core" },
|
|
4
|
+
{ "name": "Edit", "group": "Core" },
|
|
5
|
+
{ "name": "Glob", "group": "Core" },
|
|
6
|
+
{ "name": "Grep", "group": "Core" },
|
|
7
|
+
{ "name": "Read", "group": "Core" },
|
|
8
|
+
{ "name": "Write", "group": "Core" },
|
|
9
|
+
{ "name": "Skill", "group": "Core" },
|
|
10
|
+
{ "name": "WebFetch", "group": "Core" },
|
|
11
|
+
{ "name": "WebSearch", "group": "Core" },
|
|
12
|
+
{ "name": "NotebookEdit", "group": "Core" },
|
|
13
|
+
{ "name": "EnterPlanMode", "group": "Mode" },
|
|
14
|
+
{ "name": "EnterWorktree", "group": "Mode" },
|
|
15
|
+
{ "name": "TodoWrite", "group": "Task" },
|
|
16
|
+
{ "name": "TaskCreate", "group": "Task" },
|
|
17
|
+
{ "name": "TaskUpdate", "group": "Task" }
|
|
18
|
+
]
|
|
@@ -115,8 +115,13 @@ export function createModelEnvRouter({ settingsPath: staticPath } = {}) {
|
|
|
115
115
|
|
|
116
116
|
let baseChanged = false;
|
|
117
117
|
if (resolvedId) {
|
|
118
|
-
//
|
|
119
|
-
|
|
118
|
+
// When env exists in local, base MUST use the object form `{id}` so
|
|
119
|
+
// deepMerge({id}, {env}) preserves the id. With the string form,
|
|
120
|
+
// deepMerge would see a non-object base and discard it, dropping the id
|
|
121
|
+
// entirely — the bug behind empty Model ID after Duplicate/Paste.
|
|
122
|
+
// String form stays the default when there's no env, to keep JSON minimal.
|
|
123
|
+
const hasEnv = Object.keys(envIn).length > 0;
|
|
124
|
+
const nextBaseEntry = hasEnv ? { id: resolvedId } : resolvedId;
|
|
120
125
|
if (JSON.stringify(baseEntry) !== JSON.stringify(nextBaseEntry)) {
|
|
121
126
|
base.worca.models[model] = nextBaseEntry;
|
|
122
127
|
baseChanged = true;
|
package/server/project-routes.js
CHANGED
|
@@ -23,7 +23,9 @@ import { actionAllowed } from '../app/utils/state-actions.js';
|
|
|
23
23
|
import { atomicWriteSync } from './atomic-write.js';
|
|
24
24
|
import { dbExists, getIssue, listIssues } from './beads-reader.js';
|
|
25
25
|
import { dispatchExternal } from './dispatch-external.js';
|
|
26
|
+
import { migrateDispatchGovernance } from './dispatch-migration.js';
|
|
26
27
|
import { ensureWebhookForUi } from './ensure-webhook.js';
|
|
28
|
+
import { getDefaultBranch } from './git-helpers.js';
|
|
27
29
|
import { extractAndStripGlobalKeys } from './global-keys.js';
|
|
28
30
|
import { LaunchLock } from './launch-lock.js';
|
|
29
31
|
import { createModelEnvRouter } from './model-env-routes.js';
|
|
@@ -361,7 +363,8 @@ export function createProjectScopedRoutes({
|
|
|
361
363
|
router.get('/runs', requireWorcaDir, (req, res) => {
|
|
362
364
|
try {
|
|
363
365
|
const runs = discoverRuns(req.project.worcaDir);
|
|
364
|
-
const
|
|
366
|
+
const default_branch = getDefaultBranch(req.project.projectRoot);
|
|
367
|
+
const response = { ok: true, runs, default_branch };
|
|
365
368
|
// Include settings so multi-project clients can use loop limits, etc.
|
|
366
369
|
const { settingsPath } = req.project;
|
|
367
370
|
if (settingsPath && existsSync(settingsPath)) {
|
|
@@ -478,7 +481,18 @@ export function createProjectScopedRoutes({
|
|
|
478
481
|
});
|
|
479
482
|
}
|
|
480
483
|
|
|
481
|
-
|
|
484
|
+
let existingForValidation = {};
|
|
485
|
+
try {
|
|
486
|
+
if (existsSync(settingsPath)) {
|
|
487
|
+
existingForValidation = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
488
|
+
}
|
|
489
|
+
} catch {
|
|
490
|
+
existingForValidation = {};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const validation = validateSettingsPayload(body, {
|
|
494
|
+
existing: existingForValidation,
|
|
495
|
+
});
|
|
482
496
|
if (!validation.valid) {
|
|
483
497
|
return res.status(400).json({
|
|
484
498
|
error: {
|
|
@@ -504,22 +518,15 @@ export function createProjectScopedRoutes({
|
|
|
504
518
|
if (!base.worca || typeof base.worca !== 'object') base.worca = {};
|
|
505
519
|
base.worca = deepMerge(base.worca, body.worca);
|
|
506
520
|
|
|
507
|
-
if (
|
|
508
|
-
body.worca.governance &&
|
|
509
|
-
typeof body.worca.governance === 'object' &&
|
|
510
|
-
body.worca.governance.subagent_dispatch !== undefined &&
|
|
511
|
-
base.worca.governance &&
|
|
512
|
-
typeof base.worca.governance === 'object' &&
|
|
513
|
-
'dispatch' in base.worca.governance
|
|
514
|
-
) {
|
|
515
|
-
delete base.worca.governance.dispatch;
|
|
516
|
-
}
|
|
517
521
|
baseChanged = true;
|
|
518
522
|
}
|
|
519
523
|
|
|
520
524
|
// STEP 1: extract misplaced global keys + inert milestone keys
|
|
521
525
|
const autoMigrated = extractAndStripGlobalKeys(base);
|
|
522
526
|
|
|
527
|
+
// STEP 1a: migrate legacy subagent_dispatch → dispatch.subagents (W-054)
|
|
528
|
+
if (base.worca) migrateDispatchGovernance(base.worca);
|
|
529
|
+
|
|
523
530
|
// STEP 2: write extracted global keys to ~/.worca/settings.json
|
|
524
531
|
const globalSettingsPath = prefsDir
|
|
525
532
|
? join(prefsDir, 'settings.json')
|
|
@@ -558,12 +565,43 @@ export function createProjectScopedRoutes({
|
|
|
558
565
|
atomicWriteSync(settingsPath, `${JSON.stringify(base, null, 2)}\n`);
|
|
559
566
|
}
|
|
560
567
|
|
|
568
|
+
// STEP 3a: strip shadowed worca keys from settings.local.json. Local is
|
|
569
|
+
// deep-merged over base on read, so a stale `worca.<key>` copy in local
|
|
570
|
+
// would resurrect after the user saves a new value. `models` is excluded
|
|
571
|
+
// because its env-portion lives in local by design (see model-env-routes).
|
|
572
|
+
const lp = localPathFor(settingsPath);
|
|
573
|
+
let localChanged = false;
|
|
574
|
+
const localForPrune = readLocalSettings(settingsPath);
|
|
575
|
+
if (
|
|
576
|
+
body.worca &&
|
|
577
|
+
typeof body.worca === 'object' &&
|
|
578
|
+
localForPrune.worca &&
|
|
579
|
+
typeof localForPrune.worca === 'object'
|
|
580
|
+
) {
|
|
581
|
+
for (const key of Object.keys(body.worca)) {
|
|
582
|
+
if (key === 'models') continue;
|
|
583
|
+
if (key in localForPrune.worca) {
|
|
584
|
+
delete localForPrune.worca[key];
|
|
585
|
+
localChanged = true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (localChanged && Object.keys(localForPrune.worca).length === 0) {
|
|
589
|
+
delete localForPrune.worca;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
561
593
|
if (body.permissions !== undefined) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
594
|
+
localForPrune.permissions = body.permissions;
|
|
595
|
+
localChanged = true;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (localChanged) {
|
|
565
599
|
mkdirSync(dirname(lp), { recursive: true });
|
|
566
|
-
writeFileSync(
|
|
600
|
+
writeFileSync(
|
|
601
|
+
lp,
|
|
602
|
+
`${JSON.stringify(localForPrune, null, 2)}\n`,
|
|
603
|
+
'utf8',
|
|
604
|
+
);
|
|
567
605
|
}
|
|
568
606
|
|
|
569
607
|
const merged = readMergedSettings(settingsPath);
|
|
@@ -36,8 +36,12 @@ const VALID_PRICING_FIELDS = [
|
|
|
36
36
|
'cache_read_per_mtok',
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
-
export function validateSettingsPayload(body) {
|
|
39
|
+
export function validateSettingsPayload(body, options = {}) {
|
|
40
40
|
const details = [];
|
|
41
|
+
const existingWorca =
|
|
42
|
+
options.existing && typeof options.existing === 'object'
|
|
43
|
+
? options.existing.worca || {}
|
|
44
|
+
: {};
|
|
41
45
|
|
|
42
46
|
if (body.worca !== undefined) {
|
|
43
47
|
if (
|
|
@@ -49,7 +53,16 @@ export function validateSettingsPayload(body) {
|
|
|
49
53
|
return { valid: false, details };
|
|
50
54
|
}
|
|
51
55
|
const w = body.worca;
|
|
52
|
-
|
|
56
|
+
// Sections like agents/pricing reference model keys that may live in another
|
|
57
|
+
// section saved earlier. Merge persisted models with body-supplied models so
|
|
58
|
+
// a single-section save (e.g. agents-only) doesn't reject custom models.
|
|
59
|
+
const mergedModels = {
|
|
60
|
+
...(existingWorca.models && typeof existingWorca.models === 'object'
|
|
61
|
+
? existingWorca.models
|
|
62
|
+
: {}),
|
|
63
|
+
...(w.models && typeof w.models === 'object' ? w.models : {}),
|
|
64
|
+
};
|
|
65
|
+
const validModels = deriveValidModels({ models: mergedModels });
|
|
53
66
|
|
|
54
67
|
// agents
|
|
55
68
|
if (w.agents !== undefined) {
|
|
@@ -383,19 +396,91 @@ export function validateSettingsPayload(body) {
|
|
|
383
396
|
) {
|
|
384
397
|
details.push('governance.dispatch must be an object');
|
|
385
398
|
} else {
|
|
399
|
+
const DISPATCH_SECTIONS = ['tools', 'skills', 'subagents'];
|
|
386
400
|
for (const [key, val] of Object.entries(g.dispatch)) {
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
401
|
+
if (DISPATCH_SECTIONS.includes(key)) {
|
|
402
|
+
// W-054 nested shape: dispatch.{tools,skills,subagents}
|
|
403
|
+
if (
|
|
404
|
+
typeof val !== 'object' ||
|
|
405
|
+
val === null ||
|
|
406
|
+
Array.isArray(val)
|
|
407
|
+
) {
|
|
408
|
+
details.push(`governance.dispatch.${key} must be an object`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
for (const tierKey of ['always_disallowed', 'default_denied']) {
|
|
412
|
+
if (val[tierKey] === undefined) continue;
|
|
413
|
+
if (!Array.isArray(val[tierKey])) {
|
|
414
|
+
details.push(
|
|
415
|
+
`governance.dispatch.${key}.${tierKey} must be an array`,
|
|
416
|
+
);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
for (const entry of val[tierKey]) {
|
|
420
|
+
if (typeof entry !== 'string') {
|
|
421
|
+
details.push(
|
|
422
|
+
`governance.dispatch.${key}.${tierKey} entries must be strings`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (val.per_agent_allow !== undefined) {
|
|
428
|
+
if (
|
|
429
|
+
typeof val.per_agent_allow !== 'object' ||
|
|
430
|
+
val.per_agent_allow === null ||
|
|
431
|
+
Array.isArray(val.per_agent_allow)
|
|
432
|
+
) {
|
|
433
|
+
details.push(
|
|
434
|
+
`governance.dispatch.${key}.per_agent_allow must be an object`,
|
|
435
|
+
);
|
|
436
|
+
} else {
|
|
437
|
+
for (const [agent, allowList] of Object.entries(
|
|
438
|
+
val.per_agent_allow,
|
|
439
|
+
)) {
|
|
440
|
+
if (
|
|
441
|
+
agent !== '_defaults' &&
|
|
442
|
+
!VALID_AGENTS.includes(agent) &&
|
|
443
|
+
agent !== 'workspace_planner'
|
|
444
|
+
) {
|
|
445
|
+
details.push(
|
|
446
|
+
`governance.dispatch.${key}.per_agent_allow: unknown agent "${agent}"`,
|
|
447
|
+
);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (!Array.isArray(allowList)) {
|
|
451
|
+
details.push(
|
|
452
|
+
`governance.dispatch.${key}.per_agent_allow.${agent} must be an array`,
|
|
453
|
+
);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
for (const entry of allowList) {
|
|
457
|
+
if (typeof entry !== 'string') {
|
|
458
|
+
details.push(
|
|
459
|
+
`governance.dispatch.${key}.per_agent_allow.${agent} entries must be strings`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
} else if (
|
|
467
|
+
VALID_AGENTS.includes(key) ||
|
|
468
|
+
key === 'workspace_planner'
|
|
469
|
+
) {
|
|
470
|
+
// Pre-W-054 legacy flat shape — tolerated for migration.
|
|
471
|
+
if (!Array.isArray(val)) {
|
|
472
|
+
details.push(`Dispatch for "${key}" must be an array`);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
for (const v of val) {
|
|
476
|
+
if (typeof v !== 'string') {
|
|
477
|
+
details.push(
|
|
478
|
+
`Dispatch entry for "${key}" must be a string`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
398
481
|
}
|
|
482
|
+
} else {
|
|
483
|
+
details.push(`Unknown dispatch key: "${key}"`);
|
|
399
484
|
}
|
|
400
485
|
}
|
|
401
486
|
}
|
package/server/watcher.js
CHANGED
|
@@ -157,6 +157,7 @@ export function discoverRuns(worcaDir) {
|
|
|
157
157
|
...status,
|
|
158
158
|
worktree_worca_dir: join(reg.worktree_path, '.worca'),
|
|
159
159
|
is_worktree_run: true,
|
|
160
|
+
head_branch: reg.branch || null,
|
|
160
161
|
fleet_id: reg.fleet_id || null,
|
|
161
162
|
workspace_id: reg.workspace_id || null,
|
|
162
163
|
group_type: reg.group_type || null,
|
|
@@ -311,6 +312,7 @@ export async function discoverRunsAsync(worcaDir) {
|
|
|
311
312
|
...status,
|
|
312
313
|
worktree_worca_dir: join(reg.worktree_path, '.worca'),
|
|
313
314
|
is_worktree_run: true,
|
|
315
|
+
head_branch: reg.branch || null,
|
|
314
316
|
fleet_id: reg.fleet_id || null,
|
|
315
317
|
workspace_id: reg.workspace_id || null,
|
|
316
318
|
group_type: reg.group_type || null,
|
|
@@ -20,6 +20,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
20
20
|
import * as fsp from 'node:fs/promises';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
import { Router } from 'express';
|
|
23
|
+
import { getDefaultBranch } from './git-helpers.js';
|
|
23
24
|
import { pruneWorktrees, removeWorktree } from './worktree-ops.js';
|
|
24
25
|
|
|
25
26
|
const CLEANUP_CONCURRENCY = 4;
|
|
@@ -362,6 +363,7 @@ async function _listWorktrees(worcaDir) {
|
|
|
362
363
|
started_at: m.reg.started_at || null,
|
|
363
364
|
status: m.status,
|
|
364
365
|
removable: m.status !== 'running',
|
|
366
|
+
target_branch: m.reg.target_branch || null,
|
|
365
367
|
fleet_id: m.reg.fleet_id || null,
|
|
366
368
|
workspace_id: m.reg.workspace_id || null,
|
|
367
369
|
group_type: m.reg.group_type || null,
|
|
@@ -399,9 +401,11 @@ export function createWorktreesRouter() {
|
|
|
399
401
|
}
|
|
400
402
|
try {
|
|
401
403
|
const worktrees = await _listWorktrees(worcaDir);
|
|
404
|
+
const default_branch = getDefaultBranch(req.project?.projectRoot);
|
|
402
405
|
res.json({
|
|
403
406
|
ok: true,
|
|
404
407
|
worktrees,
|
|
408
|
+
default_branch,
|
|
405
409
|
// Documents the semantics shift in `disk_bytes` (project files only).
|
|
406
410
|
// Clients can render this as a caveat next to disk totals.
|
|
407
411
|
disk_walk_skip_dirs: [...WALK_SKIP_DIRS],
|
|
@@ -682,14 +682,32 @@ export function createMessageRouter({
|
|
|
682
682
|
ws.send(JSON.stringify(makeOk(req, { issues: [], runId })));
|
|
683
683
|
return;
|
|
684
684
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
685
|
+
try {
|
|
686
|
+
const issues = await listIssuesByLabel(beadsDbPath, `run:${runId}`);
|
|
687
|
+
console.log(
|
|
688
|
+
'[list-beads-by-run] runId=%s count=%d statuses=%o',
|
|
689
|
+
runId,
|
|
690
|
+
issues.length,
|
|
691
|
+
issues.map((i) => i.status),
|
|
692
|
+
);
|
|
693
|
+
ws.send(JSON.stringify(makeOk(req, { issues, runId })));
|
|
694
|
+
} catch (err) {
|
|
695
|
+
// Don't return empty issues on failure — the UI would treat that as
|
|
696
|
+
// "all beads deleted" and tear down the open <sl-details> panel. Let
|
|
697
|
+
// the client keep its last-known-good state until the next poll.
|
|
698
|
+
console.warn(
|
|
699
|
+
`[list-beads-by-run] runId=${runId} failed: ${err?.message || err}`,
|
|
700
|
+
);
|
|
701
|
+
ws.send(
|
|
702
|
+
JSON.stringify(
|
|
703
|
+
makeError(
|
|
704
|
+
req,
|
|
705
|
+
'beads_unavailable',
|
|
706
|
+
`bd query failed: ${err?.message || err}`,
|
|
707
|
+
),
|
|
708
|
+
),
|
|
709
|
+
);
|
|
710
|
+
}
|
|
693
711
|
return;
|
|
694
712
|
}
|
|
695
713
|
|