@worca/ui 0.28.0 → 0.29.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/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 not crossed-out;
1986
- * the line-through reads as "blocked", but these are present-and-required. */
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worca/ui",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Pipeline monitoring UI for worca-cc",
5
5
  "license": "MIT",
6
6
  "author": "Sinisha Djukic",
@@ -18,7 +18,22 @@ export const DISPATCH_DEFAULTS = {
18
18
  'fewer-permission-prompts',
19
19
  'loop',
20
20
  'schedule',
21
- 'worca-*',
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
- * Idempotent returns [] on already-migrated configs.
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 (!hasSubagentDispatch && !hasLegacyFlatDispatch) return changes;
205
+ if (hasSubagentDispatch || hasLegacyFlatDispatch) {
206
+ if (!gov.dispatch || Array.isArray(gov.dispatch)) gov.dispatch = {};
207
+ const dispatch = gov.dispatch;
77
208
 
78
- if (!gov.dispatch || Array.isArray(gov.dispatch)) gov.dispatch = {};
79
- const dispatch = gov.dispatch;
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
- // 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
- }
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
- 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
- }
232
+ const subagents = dispatch.subagents;
102
233
 
103
- if (!dispatch.subagents) dispatch.subagents = {};
104
- const subagents = dispatch.subagents;
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
- 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
- }
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
- 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
- }
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
- if (!dispatch.tools) {
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
- delete gov._dispatch_legacy;
262
+ changes.push(...normalizeDispatchDefaults(gov));
130
263
 
131
264
  return changes;
132
265
  }
@@ -21,6 +21,9 @@ const VALID_LOOPS = [
21
21
  'restart_planning',
22
22
  'plan_review',
23
23
  ];
24
+ const VALID_EFFORT_RUNGS = ['low', 'medium', 'high', 'xhigh', 'max'];
25
+ const VALID_AUTO_MODES = ['disabled', 'reactive', 'adaptive'];
26
+ const VALID_EFFORT_KEYS = ['auto_mode', 'auto_cap'];
24
27
  const VALID_MILESTONES = ['plan_approval', 'pr_approval', 'deploy_approval'];
25
28
  const VALID_GUARDS = [
26
29
  'block_rm_rf',
@@ -92,6 +95,52 @@ export function validateSettingsPayload(body, options = {}) {
92
95
  );
93
96
  }
94
97
  }
98
+ if (cfg.effort !== undefined) {
99
+ if (
100
+ typeof cfg.effort !== 'string' ||
101
+ !VALID_EFFORT_RUNGS.includes(cfg.effort)
102
+ ) {
103
+ details.push(
104
+ `Invalid effort "${cfg.effort}" for agent "${name}". Must be one of: ${VALID_EFFORT_RUNGS.join(', ')}`,
105
+ );
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ // effort
113
+ if (w.effort !== undefined) {
114
+ if (
115
+ typeof w.effort !== 'object' ||
116
+ w.effort === null ||
117
+ Array.isArray(w.effort)
118
+ ) {
119
+ details.push('effort must be an object');
120
+ } else {
121
+ const ef = w.effort;
122
+ for (const key of Object.keys(ef)) {
123
+ if (!VALID_EFFORT_KEYS.includes(key)) {
124
+ details.push(`Unknown effort key: "${key}"`);
125
+ }
126
+ }
127
+ if (
128
+ ef.auto_mode !== undefined &&
129
+ (typeof ef.auto_mode !== 'string' ||
130
+ !VALID_AUTO_MODES.includes(ef.auto_mode))
131
+ ) {
132
+ details.push(
133
+ `effort.auto_mode must be one of: ${VALID_AUTO_MODES.join(', ')}`,
134
+ );
135
+ }
136
+ if (
137
+ ef.auto_cap !== undefined &&
138
+ (typeof ef.auto_cap !== 'string' ||
139
+ !VALID_EFFORT_RUNGS.includes(ef.auto_cap))
140
+ ) {
141
+ details.push(
142
+ `effort.auto_cap must be one of: ${VALID_EFFORT_RUNGS.join(', ')}`,
143
+ );
95
144
  }
96
145
  }
97
146
  }
@@ -388,6 +437,16 @@ export function validateSettingsPayload(body, options = {}) {
388
437
  );
389
438
  }
390
439
  }
440
+ if (g.dispatch_migration_version !== undefined) {
441
+ if (
442
+ !Number.isInteger(g.dispatch_migration_version) ||
443
+ g.dispatch_migration_version < 0
444
+ ) {
445
+ details.push(
446
+ 'dispatch_migration_version must be a non-negative integer',
447
+ );
448
+ }
449
+ }
391
450
  if (g.dispatch !== undefined) {
392
451
  if (
393
452
  typeof g.dispatch !== 'object' ||