@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
package/app/styles.css
CHANGED
|
@@ -1928,6 +1928,25 @@ sl-input [slot="prefix"] {
|
|
|
1928
1928
|
cursor: not-allowed;
|
|
1929
1929
|
}
|
|
1930
1930
|
|
|
1931
|
+
.dispatch-suggestions .item.warn {
|
|
1932
|
+
background: var(--sl-color-warning-50);
|
|
1933
|
+
border-left: 3px solid var(--sl-color-warning-300);
|
|
1934
|
+
padding-left: 9px;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
.dispatch-suggestions .item.warn:hover,
|
|
1938
|
+
.dispatch-suggestions .item.warn.active {
|
|
1939
|
+
background: var(--sl-color-warning-100);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
.dispatch-suggestions .item-hint {
|
|
1943
|
+
font-size: var(--sl-font-size-x-small);
|
|
1944
|
+
color: var(--sl-color-warning-700);
|
|
1945
|
+
margin-left: auto;
|
|
1946
|
+
padding-left: 6px;
|
|
1947
|
+
font-style: italic;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1931
1950
|
.dispatch-suggestions .item-label {
|
|
1932
1951
|
font-size: 11px;
|
|
1933
1952
|
color: var(--sl-color-neutral-500);
|
|
@@ -1951,6 +1970,128 @@ sl-input [slot="prefix"] {
|
|
|
1951
1970
|
display: inline-block;
|
|
1952
1971
|
}
|
|
1953
1972
|
|
|
1973
|
+
/* Dispatch chip variants */
|
|
1974
|
+
.dispatch-chip-wildcard {
|
|
1975
|
+
--sl-color-neutral-200: var(--sl-color-primary-200);
|
|
1976
|
+
background: var(--sl-color-primary-100);
|
|
1977
|
+
font-weight: 600;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
.dispatch-chip-locked {
|
|
1981
|
+
text-decoration: line-through;
|
|
1982
|
+
opacity: 0.6;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
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. */
|
|
1987
|
+
.dispatch-chip-auto {
|
|
1988
|
+
text-decoration: none;
|
|
1989
|
+
opacity: 0.75;
|
|
1990
|
+
font-style: italic;
|
|
1991
|
+
border-style: dashed;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
.dispatch-chip-auto::part(base) {
|
|
1995
|
+
border-style: dashed;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
/* Lockdown placeholder — semantically distinct from a real chip. */
|
|
1999
|
+
.dispatch-chip-lockdown {
|
|
2000
|
+
text-decoration: none;
|
|
2001
|
+
opacity: 0.7;
|
|
2002
|
+
font-style: italic;
|
|
2003
|
+
text-transform: uppercase;
|
|
2004
|
+
letter-spacing: 0.5px;
|
|
2005
|
+
font-size: 10px;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
.dispatch-chip-warn {
|
|
2009
|
+
--sl-color-neutral-200: var(--sl-color-warning-200);
|
|
2010
|
+
background: var(--sl-color-warning-100);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/* Dispatch section layout */
|
|
2014
|
+
.dispatch-section {
|
|
2015
|
+
margin-bottom: 16px;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
.dispatch-section-title {
|
|
2019
|
+
font-size: 14px;
|
|
2020
|
+
font-weight: 600;
|
|
2021
|
+
margin: 12px 0 4px;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
.dispatch-section-hint {
|
|
2025
|
+
font-size: 12px;
|
|
2026
|
+
color: var(--sl-color-neutral-600);
|
|
2027
|
+
line-height: 1.4;
|
|
2028
|
+
margin: 0 0 8px;
|
|
2029
|
+
max-width: 720px;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
sl-alert.dispatch-wildcard-deny-warning {
|
|
2033
|
+
margin: 0 0 12px;
|
|
2034
|
+
font-size: 12px;
|
|
2035
|
+
max-width: 720px;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
sl-alert.dispatch-wildcard-deny-warning code {
|
|
2039
|
+
font-size: 12px;
|
|
2040
|
+
padding: 0 4px;
|
|
2041
|
+
border-radius: 3px;
|
|
2042
|
+
background: var(--sl-color-neutral-100);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
/* Collapsible wrapper for each dispatch section in the Governance tab.
|
|
2046
|
+
* Mirrors .model-env-details so users get a consistent collapse pattern. */
|
|
2047
|
+
sl-details.dispatch-section-details {
|
|
2048
|
+
margin-bottom: 0.5rem;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
sl-details.dispatch-section-details::part(base) {
|
|
2052
|
+
border: none;
|
|
2053
|
+
background: transparent;
|
|
2054
|
+
box-shadow: none;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
sl-details.dispatch-section-details::part(header) {
|
|
2058
|
+
padding: 0.4rem 0.5rem;
|
|
2059
|
+
border-radius: 6px;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
sl-details.dispatch-section-details::part(header):hover {
|
|
2063
|
+
background: var(--hover, rgba(0, 0, 0, 0.04));
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
sl-details.dispatch-section-details::part(content) {
|
|
2067
|
+
padding: 0.25rem 0 0.5rem;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
.dispatch-section-details-summary {
|
|
2071
|
+
display: flex;
|
|
2072
|
+
align-items: center;
|
|
2073
|
+
justify-content: space-between;
|
|
2074
|
+
width: 100%;
|
|
2075
|
+
gap: 0.75rem;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
.dispatch-tier {
|
|
2079
|
+
margin-bottom: 12px;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
.dispatch-tier-label {
|
|
2083
|
+
font-size: 12px;
|
|
2084
|
+
font-weight: 500;
|
|
2085
|
+
color: var(--sl-color-neutral-600);
|
|
2086
|
+
margin-bottom: 4px;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
.dispatch-tier-chips {
|
|
2090
|
+
display: flex;
|
|
2091
|
+
flex-wrap: wrap;
|
|
2092
|
+
gap: 4px;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1954
2095
|
/* Permissions list */
|
|
1955
2096
|
.settings-permissions {
|
|
1956
2097
|
display: flex;
|
|
@@ -2031,7 +2172,7 @@ sl-input [slot="prefix"] {
|
|
|
2031
2172
|
|
|
2032
2173
|
.pricing-model-name {
|
|
2033
2174
|
font-weight: 500;
|
|
2034
|
-
text-transform:
|
|
2175
|
+
text-transform: uppercase;
|
|
2035
2176
|
}
|
|
2036
2177
|
|
|
2037
2178
|
.pricing-table sl-input {
|
|
@@ -2130,6 +2271,95 @@ sl-input.pricing-input::part(input) {
|
|
|
2130
2271
|
margin: 0 2px;
|
|
2131
2272
|
}
|
|
2132
2273
|
|
|
2274
|
+
/* PR B — wider subagent fanout under wildcard defaults. The badge row needs
|
|
2275
|
+
* to wrap gracefully when an iteration dispatches 5+ distinct subagents.
|
|
2276
|
+
* Inherits flex-wrap from .iteration-tags-row; adds a small visual cue so
|
|
2277
|
+
* wildcard dispatches read differently from explicit ones. */
|
|
2278
|
+
/* One row with both sections: "Skills: <badges> Subagents: <badges>".
|
|
2279
|
+
* row-gap covers the case where chips wrap to a second line; column-gap
|
|
2280
|
+
* spaces the two section groups apart so the eye finds the boundary. */
|
|
2281
|
+
.dispatch-events-row {
|
|
2282
|
+
row-gap: 4px;
|
|
2283
|
+
column-gap: 16px;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
.dispatch-badge::part(base) {
|
|
2287
|
+
font-variant-numeric: tabular-nums;
|
|
2288
|
+
display: inline-flex;
|
|
2289
|
+
align-items: center;
|
|
2290
|
+
gap: 4px;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/* CircleCheck (allowed) / X (blocked) glyph sits inline before the badge
|
|
2294
|
+
* label, with the badge's own foreground colour. */
|
|
2295
|
+
.dispatch-badge-icon {
|
|
2296
|
+
display: inline-flex;
|
|
2297
|
+
align-items: center;
|
|
2298
|
+
line-height: 0;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
.dispatch-badge-icon svg {
|
|
2302
|
+
display: block;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
.dispatch-events-section {
|
|
2306
|
+
display: inline-flex;
|
|
2307
|
+
flex-wrap: wrap;
|
|
2308
|
+
align-items: center;
|
|
2309
|
+
gap: 4px;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/* Empty-section placeholder: keeps the label visible so layout stays
|
|
2313
|
+
* stable across iterations. Muted on purpose — explicit absence. */
|
|
2314
|
+
.dispatch-events-empty {
|
|
2315
|
+
font-size: 12px;
|
|
2316
|
+
color: var(--sl-color-neutral-500);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
.meta-value-muted {
|
|
2320
|
+
color: var(--sl-color-neutral-500);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/* Overflow control: when an iteration dispatches more than _DISPATCH_VISIBLE_LIMIT
|
|
2324
|
+
* candidates (typical under PR B's wildcard default), the tail goes inside a
|
|
2325
|
+
* compact sl-details. */
|
|
2326
|
+
sl-details.dispatch-events-overflow {
|
|
2327
|
+
display: inline-block;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
sl-details.dispatch-events-overflow::part(base) {
|
|
2331
|
+
border: none;
|
|
2332
|
+
background: transparent;
|
|
2333
|
+
box-shadow: none;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
sl-details.dispatch-events-overflow::part(header) {
|
|
2337
|
+
padding: 0 0.4rem;
|
|
2338
|
+
border-radius: 6px;
|
|
2339
|
+
font-size: 12px;
|
|
2340
|
+
color: var(--sl-color-neutral-700);
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
sl-details.dispatch-events-overflow::part(header):hover {
|
|
2344
|
+
background: var(--hover, rgba(0, 0, 0, 0.04));
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
sl-details.dispatch-events-overflow::part(content) {
|
|
2348
|
+
padding: 0.25rem 0 0;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
.dispatch-events-overflow-summary {
|
|
2352
|
+
font-size: 12px;
|
|
2353
|
+
font-weight: 500;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
.dispatch-events-overflow-content {
|
|
2357
|
+
display: flex;
|
|
2358
|
+
flex-wrap: wrap;
|
|
2359
|
+
gap: 4px;
|
|
2360
|
+
margin-top: 4px;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2133
2363
|
/* --- Agent Prompt Section --- */
|
|
2134
2364
|
sl-details.agent-prompt-section {
|
|
2135
2365
|
margin-top: 12px;
|
|
@@ -5084,6 +5314,56 @@ sl-tooltip.bead-tooltip::part(body) {
|
|
|
5084
5314
|
border-color: var(--status-failed, #ef4444);
|
|
5085
5315
|
}
|
|
5086
5316
|
|
|
5317
|
+
.rename-model-input.is-invalid::part(base) {
|
|
5318
|
+
border-color: var(--status-failed, #ef4444);
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
.rename-model-error {
|
|
5322
|
+
color: var(--status-failed, #ef4444);
|
|
5323
|
+
font-size: 12px;
|
|
5324
|
+
margin: 0.4rem 0 0;
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5327
|
+
.model-env-details::part(base) {
|
|
5328
|
+
border: none;
|
|
5329
|
+
background: transparent;
|
|
5330
|
+
box-shadow: none;
|
|
5331
|
+
}
|
|
5332
|
+
|
|
5333
|
+
.model-env-details::part(header) {
|
|
5334
|
+
padding: 0.25rem 0.4rem;
|
|
5335
|
+
border-radius: 6px;
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5338
|
+
.model-env-details::part(header):hover {
|
|
5339
|
+
background: var(--hover, rgba(0, 0, 0, 0.04));
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5342
|
+
.model-env-details::part(content) {
|
|
5343
|
+
padding: 0.5rem 0 0;
|
|
5344
|
+
}
|
|
5345
|
+
|
|
5346
|
+
.model-env-summary {
|
|
5347
|
+
display: flex;
|
|
5348
|
+
align-items: center;
|
|
5349
|
+
justify-content: space-between;
|
|
5350
|
+
width: 100%;
|
|
5351
|
+
gap: 0.75rem;
|
|
5352
|
+
}
|
|
5353
|
+
|
|
5354
|
+
.model-env-invalid-chip {
|
|
5355
|
+
color: var(--status-failed, #ef4444);
|
|
5356
|
+
font-weight: 600;
|
|
5357
|
+
font-size: 11px;
|
|
5358
|
+
text-transform: uppercase;
|
|
5359
|
+
letter-spacing: 0.5px;
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5362
|
+
.model-env-details.has-invalid::part(header) {
|
|
5363
|
+
border-left: 3px solid var(--status-failed, #ef4444);
|
|
5364
|
+
padding-left: 0.5rem;
|
|
5365
|
+
}
|
|
5366
|
+
|
|
5087
5367
|
.model-env-value::part(input) {
|
|
5088
5368
|
font-size: 12px;
|
|
5089
5369
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worca/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0-rc.1",
|
|
4
4
|
"description": "Pipeline monitoring UI for worca-cc",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sinisha Djukic",
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
"server/**/*.js",
|
|
27
27
|
"server/schemas/keys.json",
|
|
28
28
|
"server/reserved-env-keys.json",
|
|
29
|
+
"server/known-tools.json",
|
|
30
|
+
"server/known-skills.json",
|
|
29
31
|
"!server/**/*.test.js",
|
|
30
32
|
"!server/test/**",
|
|
31
33
|
"!server/**/test/**",
|
package/server/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// server/app.js
|
|
2
2
|
|
|
3
|
-
import { execFileSync, spawn } from 'node:child_process';
|
|
3
|
+
import { execFile, execFileSync, spawn } from 'node:child_process';
|
|
4
4
|
import { createHmac, randomUUID } from 'node:crypto';
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
@@ -183,6 +183,52 @@ export function createApp(options = {}) {
|
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
// GET /api/tools — static known-tools list for autocomplete.
|
|
187
|
+
const knownToolsPath = join(
|
|
188
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
189
|
+
'known-tools.json',
|
|
190
|
+
);
|
|
191
|
+
const knownTools = JSON.parse(readFileSync(knownToolsPath, 'utf8'));
|
|
192
|
+
|
|
193
|
+
app.get('/api/tools', (_req, res) => {
|
|
194
|
+
res.json({ ok: true, tools: knownTools });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// GET /api/skills — try `claude --list-skills --json`, fallback to static list.
|
|
198
|
+
const knownSkillsPath = join(
|
|
199
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
200
|
+
'known-skills.json',
|
|
201
|
+
);
|
|
202
|
+
const knownSkills = JSON.parse(readFileSync(knownSkillsPath, 'utf8'));
|
|
203
|
+
|
|
204
|
+
app.get('/api/skills', (_req, res) => {
|
|
205
|
+
execFile(
|
|
206
|
+
'claude',
|
|
207
|
+
['--list-skills', '--json'],
|
|
208
|
+
{ timeout: 5000 },
|
|
209
|
+
(err, stdout) => {
|
|
210
|
+
if (!err && stdout) {
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(stdout);
|
|
213
|
+
const skills = Array.isArray(parsed)
|
|
214
|
+
? parsed.map((s) => ({
|
|
215
|
+
name: typeof s === 'string' ? s : s.name,
|
|
216
|
+
group:
|
|
217
|
+
typeof s === 'string'
|
|
218
|
+
? 'Discovered'
|
|
219
|
+
: s.group || 'Discovered',
|
|
220
|
+
}))
|
|
221
|
+
: knownSkills;
|
|
222
|
+
return res.json({ ok: true, skills, source: 'live' });
|
|
223
|
+
} catch {
|
|
224
|
+
// JSON parse failed — fall through to fallback
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
res.json({ ok: true, skills: knownSkills, source: 'fallback' });
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
186
232
|
// GET /api/beads/issues
|
|
187
233
|
app.get('/api/beads/issues', (_req, res) => {
|
|
188
234
|
if (!worcaDir)
|
package/server/beads-reader.js
CHANGED
|
@@ -59,14 +59,21 @@ export async function listIssues(beadsDb) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export async function listIssuesByLabel(beadsDb, label) {
|
|
62
|
-
|
|
62
|
+
const attempt = async () => {
|
|
63
63
|
const issues = await runBd(
|
|
64
64
|
['list', '--label-any', label, '--all', '--limit', '0'],
|
|
65
65
|
beadsDb,
|
|
66
66
|
);
|
|
67
67
|
return await enrichWithDeps(issues, beadsDb);
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
return await attempt();
|
|
68
71
|
} catch {
|
|
69
|
-
|
|
72
|
+
// bd/SQLite contention during active runs is usually sub-second — one
|
|
73
|
+
// retry covers the observed window. If it still fails, propagate so the
|
|
74
|
+
// WS handler can return an error rather than masquerading as "no beads."
|
|
75
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
76
|
+
return await attempt();
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch governance defaults — JS mirror of Python _DISPATCH_DEFAULTS
|
|
3
|
+
* in src/worca/hooks/tracking.py.
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for the JS side; used by dispatch-migration.js
|
|
6
|
+
* and the settings editor.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const DISPATCH_DEFAULTS = {
|
|
10
|
+
tools: {
|
|
11
|
+
always_disallowed: ['EnterPlanMode', 'EnterWorktree', 'TodoWrite'],
|
|
12
|
+
default_denied: [],
|
|
13
|
+
per_agent_allow: { _defaults: ['*'] },
|
|
14
|
+
},
|
|
15
|
+
skills: {
|
|
16
|
+
always_disallowed: [
|
|
17
|
+
'batch',
|
|
18
|
+
'fewer-permission-prompts',
|
|
19
|
+
'loop',
|
|
20
|
+
'schedule',
|
|
21
|
+
'worca-*',
|
|
22
|
+
'update-config',
|
|
23
|
+
'hookify:hookify',
|
|
24
|
+
'hookify:configure',
|
|
25
|
+
'hookify:list',
|
|
26
|
+
'hookify:writing-rules',
|
|
27
|
+
'init',
|
|
28
|
+
],
|
|
29
|
+
default_denied: [
|
|
30
|
+
'claude-api',
|
|
31
|
+
'debug',
|
|
32
|
+
'review',
|
|
33
|
+
'security-review',
|
|
34
|
+
'simplify',
|
|
35
|
+
'feature-dev:feature-dev',
|
|
36
|
+
'claude-md-management:revise-claude-md',
|
|
37
|
+
'claude-md-management:claude-md-improver',
|
|
38
|
+
],
|
|
39
|
+
per_agent_allow: {
|
|
40
|
+
_defaults: ['*'],
|
|
41
|
+
implementer: ['*', 'simplify', 'claude-api'],
|
|
42
|
+
tester: ['*', 'debug'],
|
|
43
|
+
reviewer: ['*', 'review', 'security-review'],
|
|
44
|
+
learner: [
|
|
45
|
+
'*',
|
|
46
|
+
'claude-md-management:revise-claude-md',
|
|
47
|
+
'claude-md-management:claude-md-improver',
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
subagents: {
|
|
52
|
+
always_disallowed: ['general-purpose'],
|
|
53
|
+
default_denied: [],
|
|
54
|
+
per_agent_allow: { _defaults: ['*'] },
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if candidate matches any pattern in the list.
|
|
60
|
+
*
|
|
61
|
+
* Supported: exact match, trailing-* prefix glob, bare '*' (matches all).
|
|
62
|
+
* JS port of Python _matches_any() in src/worca/hooks/tracking.py (§11).
|
|
63
|
+
*
|
|
64
|
+
* @param {string} candidate
|
|
65
|
+
* @param {string[]} patterns
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function matchesAny(candidate, patterns) {
|
|
69
|
+
for (const pattern of patterns) {
|
|
70
|
+
if (pattern === candidate) return true;
|
|
71
|
+
if (pattern === '*') return true;
|
|
72
|
+
if (
|
|
73
|
+
pattern.endsWith('*') &&
|
|
74
|
+
pattern.length > 1 &&
|
|
75
|
+
candidate.startsWith(pattern.slice(0, -1))
|
|
76
|
+
) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* Works for both live and completed runs because it reads only persisted data
|
|
7
7
|
* (events.jsonl is append-only and survives the pipeline process exiting).
|
|
8
8
|
*
|
|
9
|
-
* Aggregation: events are deduplicated per iteration by (type,
|
|
10
|
-
* A `count` field tracks how many times the same
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Aggregation: events are deduplicated per iteration by (type, section, candidate).
|
|
10
|
+
* A `count` field tracks how many times the same key fired in that iteration.
|
|
11
|
+
* The `reason` from the first occurrence is kept (reasons for the same key are
|
|
12
|
+
* deterministic — derived from the denylist/rule check).
|
|
13
13
|
*
|
|
14
14
|
* Output shape per iteration:
|
|
15
15
|
* dispatch_events: [
|
|
16
|
-
* { type,
|
|
16
|
+
* { type, section, candidate, via?, reason?, count }
|
|
17
17
|
* ]
|
|
18
18
|
*/
|
|
19
19
|
|
|
@@ -29,7 +29,7 @@ const DISPATCH_EVENT_TYPES = new Set([
|
|
|
29
29
|
* Malformed lines are silently skipped so a corrupt event doesn't break the run view.
|
|
30
30
|
*
|
|
31
31
|
* @param {string} eventsPath — absolute path to events.jsonl
|
|
32
|
-
* @returns {Array<{type,
|
|
32
|
+
* @returns {Array<{type, section, candidate, via?, reason?, timestamp}>}
|
|
33
33
|
*/
|
|
34
34
|
export function readDispatchEventsFromJsonl(eventsPath) {
|
|
35
35
|
if (!eventsPath || !existsSync(eventsPath)) return [];
|
|
@@ -50,13 +50,17 @@ export function readDispatchEventsFromJsonl(eventsPath) {
|
|
|
50
50
|
}
|
|
51
51
|
if (!DISPATCH_EVENT_TYPES.has(e.event_type)) continue;
|
|
52
52
|
const payload = e.payload || {};
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const candidate = payload.candidate;
|
|
54
|
+
if (!candidate) continue;
|
|
55
|
+
const entry = {
|
|
55
56
|
type: e.event_type,
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
section: payload.section || 'subagents',
|
|
58
|
+
candidate,
|
|
58
59
|
timestamp: e.timestamp,
|
|
59
|
-
}
|
|
60
|
+
};
|
|
61
|
+
if (payload.reason) entry.reason = payload.reason;
|
|
62
|
+
if (payload.via) entry.via = payload.via;
|
|
63
|
+
out.push(entry);
|
|
60
64
|
}
|
|
61
65
|
return out;
|
|
62
66
|
}
|
|
@@ -64,13 +68,13 @@ export function readDispatchEventsFromJsonl(eventsPath) {
|
|
|
64
68
|
/**
|
|
65
69
|
* Given a list of dispatch events and a stages map from status.json, return
|
|
66
70
|
* a new stages map where each iteration that overlaps an event's timestamp
|
|
67
|
-
* is enriched with a `dispatch_events` array (deduplicated by type+
|
|
71
|
+
* is enriched with a `dispatch_events` array (deduplicated by type+section+candidate
|
|
68
72
|
* with a count).
|
|
69
73
|
*
|
|
70
74
|
* Non-destructive: input stages object is shallow-copied; iterations get new
|
|
71
75
|
* objects with the extra field. Existing iteration fields are preserved.
|
|
72
76
|
*
|
|
73
|
-
* @param {Array<{type,
|
|
77
|
+
* @param {Array<{type, section, candidate, via?, reason?, timestamp}>} events
|
|
74
78
|
* @param {object} stages — status.stages
|
|
75
79
|
* @returns {object} enriched stages
|
|
76
80
|
*/
|
|
@@ -134,26 +138,28 @@ export function assignEventsToIterations(events, stages) {
|
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
/**
|
|
137
|
-
* Deduplicate an array of dispatch events by (type,
|
|
138
|
-
* occurrences. First reason wins for blocked events.
|
|
141
|
+
* Deduplicate an array of dispatch events by (type, section, candidate) and
|
|
142
|
+
* count occurrences. First reason wins for blocked events.
|
|
139
143
|
*
|
|
140
|
-
* @param {Array<{type,
|
|
141
|
-
* @returns {Array<{type,
|
|
144
|
+
* @param {Array<{type, section, candidate, via?, reason?}>} events
|
|
145
|
+
* @returns {Array<{type, section, candidate, via?, reason?, count}>}
|
|
142
146
|
*/
|
|
143
147
|
function aggregate(events) {
|
|
144
148
|
const map = new Map();
|
|
145
149
|
for (const ev of events) {
|
|
146
|
-
const key = `${ev.type}|${ev.
|
|
150
|
+
const key = `${ev.type}|${ev.section}|${ev.candidate}`;
|
|
147
151
|
const existing = map.get(key);
|
|
148
152
|
if (existing) {
|
|
149
153
|
existing.count += 1;
|
|
150
154
|
} else {
|
|
151
155
|
const entry = {
|
|
152
156
|
type: ev.type,
|
|
153
|
-
|
|
157
|
+
section: ev.section,
|
|
158
|
+
candidate: ev.candidate,
|
|
154
159
|
count: 1,
|
|
155
160
|
};
|
|
156
161
|
if (ev.reason) entry.reason = ev.reason;
|
|
162
|
+
if (ev.via) entry.via = ev.via;
|
|
157
163
|
map.set(key, entry);
|
|
158
164
|
}
|
|
159
165
|
}
|