@worca/app 1.3.0 → 1.4.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/README.md +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
|
@@ -14,7 +14,8 @@ import { join, resolve, isAbsolute, sep } from 'node:path';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { worcaHome } from './projects.mjs'; // user agent layer root (read fresh per call)
|
|
16
16
|
import { readPluginsLock, pluginCurrentDir } from './plugins-lock.mjs'; // plugin layer roots (Task 2)
|
|
17
|
-
import { declaredApi, NOT_META_V2 } from './plugin-manifest.mjs'; // plugin API declared by a layer's manifest
|
|
17
|
+
import { declaredApi, negotiatedApi, NOT_META_V2, ASK_NEEDS_API_4 } from './plugin-manifest.mjs'; // plugin API declared/negotiated by a layer's manifest
|
|
18
|
+
import { WORCA_ASK_FORMS_API } from './plugin-api.mjs';
|
|
18
19
|
import { normalizeAgentMeta, DEFAULT_ORDER } from '../shared/graph/agent-meta.mjs'; // meta v2 (one source: registry + store + UI)
|
|
19
20
|
import { MOCK_WRITER_ROLES } from './claude-runner.mjs'; // mockRole vocabulary (no cycle: claude-runner imports no registry)
|
|
20
21
|
import { readFrontmatterSync } from './frontmatter.mjs';
|
|
@@ -84,7 +85,7 @@ const AGENT_KEY_RE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
|
84
85
|
* `warn` is INJECTABLE (defaults to console.warn) so scanLayer can capture the
|
|
85
86
|
* reason a sidecar was dropped and hand it to a diagnostics sink — the reason
|
|
86
87
|
* is authored here and must never be re-derived by a second reader. */
|
|
87
|
-
export function normalizeMeta(raw, { warn = console.warn } = {}) {
|
|
88
|
+
export function normalizeMeta(raw, { warn = console.warn, onDropForm = null } = {}) {
|
|
88
89
|
if (!raw || typeof raw !== 'object') return null;
|
|
89
90
|
const key = typeof raw.key === 'string' ? raw.key.trim() : '';
|
|
90
91
|
if (!key) return null;
|
|
@@ -149,6 +150,7 @@ export function normalizeMeta(raw, { warn = console.warn } = {}) {
|
|
|
149
150
|
const { meta, errors } = normalizeAgentMeta(raw, {
|
|
150
151
|
mockWriterRoles: MOCK_WRITER_ROLES,
|
|
151
152
|
warn: (msg) => warn(msg),
|
|
153
|
+
onDropForm, // §3: a dropped FORM must not look like a dropped SIDECAR
|
|
152
154
|
});
|
|
153
155
|
if (errors.length) {
|
|
154
156
|
warn(`[agent-registry] sidecar "${key}" declares metaVersion 2 but is invalid; skipped: ${errors.join('; ')}`);
|
|
@@ -162,7 +164,7 @@ export function normalizeMeta(raw, { warn = console.warn } = {}) {
|
|
|
162
164
|
portSummary: meta.portSummary,
|
|
163
165
|
};
|
|
164
166
|
for (const field of ['verdict', 'sideEffect', 'mockRole', 'wantsRequest', 'workspaceFanOut',
|
|
165
|
-
'workspaceStrategy', 'workspaceVariantOf', 'placeable']) {
|
|
167
|
+
'workspaceStrategy', 'workspaceVariantOf', 'placeable', 'ask', 'humanEffort']) {
|
|
166
168
|
if (field in meta) merged[field] = meta[field];
|
|
167
169
|
}
|
|
168
170
|
return merged;
|
|
@@ -181,16 +183,18 @@ export function userAgentsDir() {
|
|
|
181
183
|
|
|
182
184
|
/**
|
|
183
185
|
* Third registry layer (spec §9.1): every ENABLED installed plugin's
|
|
184
|
-
* current
|
|
185
|
-
* collision winner among plugins.
|
|
186
|
+
* current/<subdir> dir, in lexicographic plugin-name order — the deterministic
|
|
187
|
+
* collision winner among plugins. Shared by the agent registry (`agents`) and the
|
|
188
|
+
* script registry (`scripts`). An entry is skipped when disabled
|
|
186
189
|
* (enabled === false in the lock) or broken (existsSync follows the current/
|
|
187
190
|
* symlink, so a missing or dangling symlink — and a version dir without
|
|
188
|
-
*
|
|
191
|
+
* <subdir>/ — drops out). Wrapped in try/catch like userAgentsDir(): with no
|
|
189
192
|
* resolvable worca-cc home (bare node:test runner) or an unreadable lock this
|
|
190
193
|
* returns [] and registry loads never throw.
|
|
191
|
-
* @
|
|
194
|
+
* @param {'agents'|'scripts'} subdir
|
|
195
|
+
* @returns {Array<{plugin: string, dir: string, builtFor: number|null, api: number|null}>}
|
|
192
196
|
*/
|
|
193
|
-
export function
|
|
197
|
+
export function pluginLayers(subdir) {
|
|
194
198
|
try {
|
|
195
199
|
const lock = readPluginsLock();
|
|
196
200
|
return Object.keys(lock)
|
|
@@ -199,14 +203,22 @@ export function pluginAgentLayers() {
|
|
|
199
203
|
.map((name) => {
|
|
200
204
|
const dir = pluginCurrentDir(name);
|
|
201
205
|
let builtFor = null;
|
|
206
|
+
let api = null;
|
|
202
207
|
try {
|
|
203
208
|
const raw = JSON.parse(readFileSync(join(dir, 'worca-cc-plugin.json'), 'utf8'));
|
|
209
|
+
const range = raw?.engines?.['worca-cc-api'] ?? '';
|
|
204
210
|
// `|| null`: declaredApi('') is 0 (an unconstrained range accepts
|
|
205
211
|
// everything), and "built for plugin API 0" is not English. apiMismatch
|
|
206
212
|
// guards the same case the same way.
|
|
207
|
-
builtFor = declaredApi(
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
builtFor = declaredApi(range) || null;
|
|
214
|
+
// builtFor is the LOWEST integer the range accepts (what the plugin was
|
|
215
|
+
// written against); `api` is the HIGHEST host API it admits — what the
|
|
216
|
+
// plugin actually NEGOTIATES, and therefore what decides which host
|
|
217
|
+
// features it gets. `null` when the manifest is missing or unreadable:
|
|
218
|
+
// every feature gate below must fail CLOSED on it.
|
|
219
|
+
api = negotiatedApi(range);
|
|
220
|
+
} catch { builtFor = null; api = null; } // unreadable manifest: the message degrades, the skip does not
|
|
221
|
+
return { plugin: name, dir: join(dir, subdir), builtFor, api };
|
|
210
222
|
})
|
|
211
223
|
.filter(({ dir }) => existsSync(dir));
|
|
212
224
|
} catch {
|
|
@@ -214,14 +226,23 @@ export function pluginAgentLayers() {
|
|
|
214
226
|
}
|
|
215
227
|
}
|
|
216
228
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
export function pluginAgentLayers() { return pluginLayers('agents'); }
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Scan one layer dir for `*.meta.json` and normalize each through
|
|
233
|
+
* `normalize(parsed, { warn })` (null = skip). Stamps the COMPUTED `origin`.
|
|
234
|
+
* ONE reader for "a directory of sidecars", two normalizers (agents, scripts).
|
|
235
|
+
* `tag` prefixes the warnings (`[agent-registry]` / `[script-registry]`).
|
|
236
|
+
* @returns {Array<{meta: object, file: string}>}
|
|
237
|
+
*/
|
|
238
|
+
export function scanMetaLayer(dir, origin, { normalize, tag, requireMetaV2 = false, builtFor = null, onDrop = null }) {
|
|
221
239
|
// Every skip below is a CONTRIBUTION THE USER CANNOT SEE unless someone
|
|
222
240
|
// reports it: console.warn reaches a server log, not the Plugins card, the
|
|
223
241
|
// install receipt or the doctor. onDrop is that reporting channel — optional,
|
|
224
|
-
// so the hot registry path pays nothing when nobody is listening.
|
|
242
|
+
// so the hot registry path pays nothing when nobody is listening. A drop means
|
|
243
|
+
// "this file, or a declared PART of it, was ignored": scanLayer's API-4 gate
|
|
244
|
+
// reports a stripped `ask` block through the same channel, and the reason
|
|
245
|
+
// string is what tells the two apart.
|
|
225
246
|
const drop = (file, reason) => { if (onDrop) onDrop({ origin, file, reason }); };
|
|
226
247
|
let files;
|
|
227
248
|
try {
|
|
@@ -229,7 +250,7 @@ function scanLayer(dir, origin, { requireMetaV2 = false, builtFor = null, onDrop
|
|
|
229
250
|
} catch {
|
|
230
251
|
return []; // missing layer dir => empty layer (fails safe)
|
|
231
252
|
}
|
|
232
|
-
const
|
|
253
|
+
const out = [];
|
|
233
254
|
for (const f of files) {
|
|
234
255
|
if (!f.endsWith('.meta.json')) continue;
|
|
235
256
|
let parsed;
|
|
@@ -246,18 +267,35 @@ function scanLayer(dir, origin, { requireMetaV2 = false, builtFor = null, onDrop
|
|
|
246
267
|
// layers keep the v1 path until the engine cut-over.
|
|
247
268
|
if (requireMetaV2 && Number(parsed?.metaVersion) !== 2) {
|
|
248
269
|
const builtForText = builtFor == null ? 'an older plugin API' : `plugin API ${builtFor}`;
|
|
249
|
-
console.warn(`[
|
|
270
|
+
console.warn(`[${tag}] ${origin}/${f}: built for ${builtForText} — ${NOT_META_V2} — ignored`);
|
|
250
271
|
drop(f, `built for ${builtForText} — ${NOT_META_V2}`);
|
|
251
272
|
continue;
|
|
252
273
|
}
|
|
253
|
-
// Capture
|
|
274
|
+
// Capture the normalizer's own reason rather than re-deriving one: the LAST
|
|
254
275
|
// warning it emits is the fatal one (non-fatal coercion warnings precede it).
|
|
255
276
|
let why = '';
|
|
256
|
-
const
|
|
257
|
-
|
|
277
|
+
const prefix = `[${tag}] `;
|
|
278
|
+
const meta = normalize(parsed, {
|
|
279
|
+
warn: (m) => { why = String(m).startsWith(prefix) ? String(m).slice(prefix.length) : String(m); console.warn(m); },
|
|
280
|
+
// A form that fails gate 1 drops the FORM, not the sidecar, so it reports
|
|
281
|
+
// straight to the diagnostics sink instead of through `why` (which names
|
|
282
|
+
// the reason an agent vanished). The script normalizer ignores the opt.
|
|
283
|
+
onDropForm: ({ message }) => drop(f, message),
|
|
258
284
|
});
|
|
259
285
|
if (!meta) { drop(f, why || 'invalid sidecar'); continue; }
|
|
260
286
|
meta.origin = origin; // computed, never stored
|
|
287
|
+
out.push({ meta, file: f });
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Scan one agent layer dir for *.meta.json; stamps the COMPUTED origin/agentPath/
|
|
293
|
+
* descriptionDerived fields (none of which normalizeMeta returns, so none can
|
|
294
|
+
* be persisted back into a sidecar). */
|
|
295
|
+
function scanLayer(dir, origin, { requireMetaV2 = false, builtFor = null, onDrop = null, askApi = null } = {}) {
|
|
296
|
+
const drop = (file, reason) => { if (onDrop) onDrop({ origin, file, reason }); };
|
|
297
|
+
const metas = [];
|
|
298
|
+
for (const { meta, file: f } of scanMetaLayer(dir, origin, { normalize: normalizeMeta, tag: 'agent-registry', requireMetaV2, builtFor, onDrop })) {
|
|
261
299
|
// agentFile is a PATH read as the agent's system prompt AND for its
|
|
262
300
|
// `tools:` frontmatter, so the loader refuses to stamp an agentPath outside
|
|
263
301
|
// the layer it is scanning. Belt-and-braces behind validatePluginDir, which
|
|
@@ -268,6 +306,20 @@ function scanLayer(dir, origin, { requireMetaV2 = false, builtFor = null, onDrop
|
|
|
268
306
|
drop(f, `agentFile "${meta.agentFile}" resolves outside the agents dir`);
|
|
269
307
|
continue;
|
|
270
308
|
}
|
|
309
|
+
|
|
310
|
+
// API 4 (plugin layers only — `requireMetaV2` is the same plugin-layer
|
|
311
|
+
// marker the meta v2 gate above rides): an agent's `ask` block is honoured
|
|
312
|
+
// only when the plugin NEGOTIATES plugin API 4. Below that it is stripped
|
|
313
|
+
// HERE, at the single choke point, so the prompt block, the ask-time gate,
|
|
314
|
+
// the Agents view and History all see an agent that simply has no forms and
|
|
315
|
+
// the agent falls back to generic questions on its own (ask-forms spec §10).
|
|
316
|
+
// Number(null) is 0, so an unknowable API fails CLOSED. The sidecar itself
|
|
317
|
+
// is NOT dropped: its ports are fine and its agent stays usable.
|
|
318
|
+
if (requireMetaV2 && meta.ask && !(Number(askApi) >= WORCA_ASK_FORMS_API)) {
|
|
319
|
+
console.warn(`[agent-registry] ${origin}/${f}: ${ASK_NEEDS_API_4}`);
|
|
320
|
+
drop(f, ASK_NEEDS_API_4);
|
|
321
|
+
delete meta.ask;
|
|
322
|
+
}
|
|
271
323
|
meta.agentPath = meta.agentFile ? join(dir, meta.agentFile) : null; // layer-correct abs path
|
|
272
324
|
// The agent .md's frontmatter (name/description/tools/model), read from the
|
|
273
325
|
// file HEAD only (frontmatter.mjs) — COMPUTED like origin/agentPath, never
|
|
@@ -328,8 +380,8 @@ export function loadAgentRegistry(agentsDir = DEFAULT_AGENTS_DIR, opts = {}) {
|
|
|
328
380
|
const plugins = [];
|
|
329
381
|
if (opts.includePlugins !== false) {
|
|
330
382
|
const taken = new Set([...builtinKeys, ...users.map((m) => m.key)]);
|
|
331
|
-
for (const { plugin, dir, builtFor } of pluginAgentLayers()) {
|
|
332
|
-
for (const m of scanLayer(dir, `plugin:${plugin}`, { requireMetaV2: true, builtFor, onDrop })) {
|
|
383
|
+
for (const { plugin, dir, builtFor, api } of pluginAgentLayers()) {
|
|
384
|
+
for (const m of scanLayer(dir, `plugin:${plugin}`, { requireMetaV2: true, builtFor, askApi: api, onDrop })) {
|
|
333
385
|
if (taken.has(m.key)) {
|
|
334
386
|
console.warn(
|
|
335
387
|
`[agent-registry] plugin agent "${m.key}" (plugin "${plugin}") collides with an existing agent and was skipped`,
|
package/src/core/agent-store.mjs
CHANGED
|
@@ -11,6 +11,9 @@ import { listWorkflows } from './workflows.mjs';
|
|
|
11
11
|
import { validateMetaV2 } from '../shared/graph/agent-meta.mjs';
|
|
12
12
|
import { AWAIT_PORT } from '../shared/graph/constants.mjs'; // the synthesized gate port is wirable
|
|
13
13
|
|
|
14
|
+
import { normalizeAskBlock, validateFormDef } from '../shared/forms/form-def.mjs';
|
|
15
|
+
import { ASK_LIMITS } from '../shared/forms/catalog.mjs';
|
|
16
|
+
|
|
14
17
|
export { userAgentsDir }; // single source: the Phase 1 layer resolver
|
|
15
18
|
|
|
16
19
|
/** A key is a bare alphanumeric stem — can never contain "/" or "..". */
|
|
@@ -24,8 +27,48 @@ function err(message, code) { return Object.assign(new Error(message), { code })
|
|
|
24
27
|
* `scope:'workspace-only'` can never be undone from the editor. Everything else
|
|
25
28
|
* (including the v1 wiring the registry still derives, which P8 owns) MERGES. */
|
|
26
29
|
const V2_CLEARABLE = ['verdict', 'sideEffect', 'mockRole', 'wantsRequest', 'workspaceFanOut',
|
|
27
|
-
'workspaceStrategy', 'workspaceVariantOf', 'placeable', 'scope', 'domain', 'icon',
|
|
28
|
-
'promptHints', 'requiresSkills'];
|
|
30
|
+
'workspaceStrategy', 'workspaceVariantOf', 'placeable', 'scope', 'domain', 'icon', 'ask',
|
|
31
|
+
'promptHints', 'requiresSkills', 'humanEffort'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* GATE 1 for a sidecar's ask forms (ask-forms spec §5, §11). The SAME checks the
|
|
35
|
+
* registry runs at load and `worca plugin validate` runs on a plugin dir — one
|
|
36
|
+
* validator, three callers. Returns [] when the sidecar declares no forms.
|
|
37
|
+
* Every failed rule is named: the editor marks fields by `path`, so a single
|
|
38
|
+
* "invalid form" sentence would be unusable there.
|
|
39
|
+
* @param {object} raw a raw sidecar (pre-normalizeMeta)
|
|
40
|
+
* @returns {Array<{path: string, code: string, message: string}>}
|
|
41
|
+
*/
|
|
42
|
+
function askFormIssues(raw) {
|
|
43
|
+
if (!raw || raw.ask === undefined || raw.ask === null) return [];
|
|
44
|
+
// normalizeAskBlock is used ONLY for the whole-block refusals it alone knows
|
|
45
|
+
// (id '*': not `{ forms }`, or over the 64 KB cap). Its per-form `reason` is a
|
|
46
|
+
// collapsed one-liner of the first three errors — right for a registry log
|
|
47
|
+
// line, wrong for an editor that marks fields BY PATH — so each form goes
|
|
48
|
+
// through validateFormDef directly and keeps P1's paths verbatim
|
|
49
|
+
// (`layout#<n>`, a depth-first 1-based ordinal).
|
|
50
|
+
const blockDrops = normalizeAskBlock(raw.ask).dropped.filter((d) => d.id === '*');
|
|
51
|
+
if (blockDrops.length) return blockDrops.map((d) => ({ path: 'ask', code: 'dialect', message: d.reason }));
|
|
52
|
+
const issues = [];
|
|
53
|
+
const forms = raw.ask.forms && typeof raw.ask.forms === 'object' ? raw.ask.forms : {};
|
|
54
|
+
const ids = Object.keys(forms);
|
|
55
|
+
if (ids.length > ASK_LIMITS.formsPerAgent) {
|
|
56
|
+
issues.push({ path: 'ask.forms', code: 'too-many', message: `at most ${ASK_LIMITS.formsPerAgent} forms per agent` });
|
|
57
|
+
}
|
|
58
|
+
for (const id of ids) {
|
|
59
|
+
for (const e of validateFormDef(forms[id], { id }).errors) {
|
|
60
|
+
issues.push({ path: `ask.forms."${id}"${e.path ? ` ${e.path}` : ''}`, code: e.code, message: e.message });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return issues;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The ONE throw shape both write paths use for a failed gate 1: a joined message
|
|
67
|
+
* for CLI/log surfaces, plus the structured list the route turns into a 422 body. */
|
|
68
|
+
function askFormError(issues) {
|
|
69
|
+
return Object.assign(new Error(issues.map((i) => `${i.path}: ${i.message}`).join('; ')),
|
|
70
|
+
{ code: 'ASK_FORM', errors: issues });
|
|
71
|
+
}
|
|
29
72
|
|
|
30
73
|
/** The writable user layer dir. userAgentsDir() returns null only when the home
|
|
31
74
|
* cannot be resolved (no WORCA_HOME under node:test) — surface that as a 400. */
|
|
@@ -76,6 +119,10 @@ export async function createAgent({ meta: rawMeta, markdown } = {}) {
|
|
|
76
119
|
// wrote — or rejected with "invalid agent metadata". Every failed rule is named.
|
|
77
120
|
const issues = validateMetaV2(raw).errors;
|
|
78
121
|
if (issues.length) throw err(issues.join('; '), 'BAD_REQUEST');
|
|
122
|
+
// Gate 1 runs AFTER the meta gate and BEFORE normalizeMeta: normalizeMeta is
|
|
123
|
+
// lossy by design, so a form must be judged as the author wrote it.
|
|
124
|
+
const formIssues = askFormIssues(raw);
|
|
125
|
+
if (formIssues.length) throw askFormError(formIssues);
|
|
79
126
|
const meta = normalizeMeta(raw);
|
|
80
127
|
if (!meta) throw err('invalid agent metadata', 'BAD_REQUEST');
|
|
81
128
|
const existing = loadAgentRegistry()[key];
|
|
@@ -241,6 +288,8 @@ export async function updateAgent(key, { meta: rawMeta, markdown } = {}) {
|
|
|
241
288
|
// The same gate the create path applies: every failed rule named, nothing written.
|
|
242
289
|
const updIssues = validateMetaV2(raw).errors;
|
|
243
290
|
if (updIssues.length) throw err(updIssues.join('; '), 'BAD_REQUEST');
|
|
291
|
+
const updFormIssues = askFormIssues(raw);
|
|
292
|
+
if (updFormIssues.length) throw askFormError(updFormIssues);
|
|
244
293
|
const meta = normalizeMeta(raw);
|
|
245
294
|
if (!meta) throw err('invalid agent metadata', 'BAD_REQUEST');
|
|
246
295
|
const dir = requireUserDir();
|
package/src/core/artifacts.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { branchExists, diffShortstat, hasGh, findPrForBranch } from './git-info.
|
|
|
19
19
|
import { getDb, tx } from './db.mjs';
|
|
20
20
|
import { RUN_LOG_FILE } from './run-log.mjs';
|
|
21
21
|
import { readRunLedger } from './metrics/ledger.mjs';
|
|
22
|
+
import { readPolicyState } from './policy/state.mjs';
|
|
22
23
|
import { memoryTotals } from './memory-sync.mjs';
|
|
23
24
|
|
|
24
25
|
// ── DB row <-> state object mapping (Phase 3) ──────────────────────────────────
|
|
@@ -249,6 +250,36 @@ export async function writeStepQuestions(pipelineId, stepKey, round, { agentKey,
|
|
|
249
250
|
}
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Spec §9: a form ask persists into the SAME schemaless JSON TEXT columns as the
|
|
255
|
+
* legacy Q&A — the `questions` half holds the full resolved ask, the `answers`
|
|
256
|
+
* half `{kind:'form', form, version, values}`. This merges the two into the one
|
|
257
|
+
* object History renders, or null for a legacy row. The legacy arrays are read
|
|
258
|
+
* separately and stay empty for a form row, so no existing reader changes.
|
|
259
|
+
* @param {object|null} qWrap parsed `questions` column
|
|
260
|
+
* @param {object|null} aWrap parsed `answers` column
|
|
261
|
+
*/
|
|
262
|
+
function formAskOf(qWrap, aWrap) {
|
|
263
|
+
if (!qWrap || qWrap.kind !== 'form') return null;
|
|
264
|
+
const values = aWrap && aWrap.kind === 'form' && aWrap.values && typeof aWrap.values === 'object'
|
|
265
|
+
? aWrap.values
|
|
266
|
+
: null;
|
|
267
|
+
return { ...qWrap, values };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The two form fields a reader row carries — `{}` for a legacy row, so no legacy
|
|
271
|
+
* payload gains a key (test/step-questions-db.test.mjs pins the exact row shape). */
|
|
272
|
+
function formFieldsOf(qWrap, aWrap) {
|
|
273
|
+
const ask = formAskOf(qWrap, aWrap);
|
|
274
|
+
return ask ? { ask, formAnswer: formAnswerOf(aWrap) } : {};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The `{form, version, values}` payload the agent was resumed with, or null. */
|
|
278
|
+
function formAnswerOf(aWrap) {
|
|
279
|
+
if (!aWrap || aWrap.kind !== 'form') return null;
|
|
280
|
+
return { form: aWrap.form, version: aWrap.version, values: aWrap.values && typeof aWrap.values === 'object' ? aWrap.values : {} };
|
|
281
|
+
}
|
|
282
|
+
|
|
252
283
|
/**
|
|
253
284
|
* All ask-then-resume rounds of a pipeline, unwrapped to plain arrays, in
|
|
254
285
|
* chronological insert order (rowid — lexicographic step_key would mis-order
|
|
@@ -270,6 +301,10 @@ export function readStepQuestions(pipelineId) {
|
|
|
270
301
|
agentKey: r.agent_key || '',
|
|
271
302
|
questions: Array.isArray(qWrap?.questions) ? qWrap.questions : [],
|
|
272
303
|
answers: Array.isArray(aWrap?.answers) ? aWrap.answers : [],
|
|
304
|
+
// Ask forms (spec §9): `ask` + `formAnswer` on a FORM row only. A legacy row
|
|
305
|
+
// gains no key at all, so its wire shape (History, get_run_progress) stays
|
|
306
|
+
// byte-identical; consumers test `row.ask`, never `'ask' in row`.
|
|
307
|
+
...formFieldsOf(qWrap, aWrap),
|
|
273
308
|
};
|
|
274
309
|
});
|
|
275
310
|
}
|
|
@@ -348,6 +383,7 @@ export function readPipelineExtras(pipelineId) {
|
|
|
348
383
|
const clarify = {
|
|
349
384
|
questions: Array.isArray(qWrap?.questions) ? qWrap.questions : [],
|
|
350
385
|
answers: Array.isArray(aWrap?.answers) ? aWrap.answers : [],
|
|
386
|
+
...formFieldsOf(qWrap, aWrap), // spec §9: `ask` + `formAnswer` on a form row only
|
|
351
387
|
};
|
|
352
388
|
const reviews = getDb().prepare(
|
|
353
389
|
'SELECT kind, cycle, verdict FROM reviews WHERE pipeline_id = ? ORDER BY kind, cycle'
|
|
@@ -1128,15 +1164,16 @@ export async function writeState(pipelineDir, stateObj) {
|
|
|
1128
1164
|
INSERT INTO pipelines (id, project_key, workspace_key, target, title, base_name,
|
|
1129
1165
|
date_prefix, status, phase, cycle, started_at, updated_at, total_cost_usd,
|
|
1130
1166
|
total_active_ms, prompt, branch, workspace_meta, stepper, tools, resume_point,
|
|
1131
|
-
source_type, source_ref, guardrails_id, outcome)
|
|
1167
|
+
source_type, source_ref, guardrails_id, outcome, human_hours)
|
|
1132
1168
|
VALUES (@id,@project_key,@workspace_key,@target,@title,@base_name,@date_prefix,
|
|
1133
1169
|
@status,@phase,@cycle,@started_at,@updated_at,@total_cost_usd,@total_active_ms,
|
|
1134
1170
|
@prompt,@branch,@workspace_meta,@stepper,@tools,@resume_point,
|
|
1135
|
-
@source_type,@source_ref,@guardrails_id,@outcome)
|
|
1171
|
+
@source_type,@source_ref,@guardrails_id,@outcome,@human_hours)
|
|
1136
1172
|
ON CONFLICT(id) DO UPDATE SET
|
|
1137
1173
|
status=excluded.status, phase=excluded.phase, cycle=excluded.cycle,
|
|
1138
1174
|
updated_at=excluded.updated_at, total_cost_usd=excluded.total_cost_usd,
|
|
1139
|
-
total_active_ms=excluded.total_active_ms,
|
|
1175
|
+
total_active_ms=excluded.total_active_ms, human_hours=excluded.human_hours,
|
|
1176
|
+
branch=excluded.branch,
|
|
1140
1177
|
workspace_meta=excluded.workspace_meta, stepper=excluded.stepper,
|
|
1141
1178
|
tools=excluded.tools,
|
|
1142
1179
|
resume_point=excluded.resume_point,
|
|
@@ -1150,16 +1187,22 @@ export async function writeState(pipelineDir, stateObj) {
|
|
|
1150
1187
|
INSERT INTO pipeline_steps (pipeline_id, key, node_id, phase, step_index, cycle,
|
|
1151
1188
|
status, started_at, updated_at, active_ms, running_since, cost_usd, session_id,
|
|
1152
1189
|
skills, graphify_count,
|
|
1153
|
-
execution_id, exec_kind, agent_key, ended_at, exec_trigger, exec_result, exec_meta
|
|
1154
|
-
|
|
1190
|
+
execution_id, exec_kind, agent_key, ended_at, exec_trigger, exec_result, exec_meta,
|
|
1191
|
+
human_hours, human_signals)
|
|
1192
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
1155
1193
|
`);
|
|
1156
1194
|
for (const st of Array.isArray(obj.steps) ? obj.steps : []) {
|
|
1157
1195
|
// v2 rows: execution_id === key. v1 rows leave every exec_* column NULL, so
|
|
1158
1196
|
// the readers below reproduce today's exact shape for a v1 pipeline.
|
|
1159
|
-
const
|
|
1197
|
+
const hasMeta = st.taskId != null || st.parentExecutionId != null || st.title != null || st.phaseOrdinal != null
|
|
1198
|
+
|| st.nodeKey != null || st.runtime != null || st.exitCode != null || st.bridgeCalls != null;
|
|
1199
|
+
const meta = hasMeta
|
|
1160
1200
|
? s({ taskId: st.taskId ?? null, parentExecutionId: st.parentExecutionId ?? null,
|
|
1161
1201
|
title: st.title ?? null, phaseOrdinal: st.phaseOrdinal ?? null,
|
|
1162
|
-
taskIndex: st.taskIndex ?? null, taskTotal: st.taskTotal ?? null
|
|
1202
|
+
taskIndex: st.taskIndex ?? null, taskTotal: st.taskTotal ?? null,
|
|
1203
|
+
nodeKey: st.nodeKey ?? null, runtime: st.runtime ?? null, exitCode: st.exitCode ?? null,
|
|
1204
|
+
// Model bridge (§8.6): requests the node initiated through the bridge.
|
|
1205
|
+
...(st.bridgeCalls != null ? { bridgeCalls: st.bridgeCalls, bridgeContinued: st.bridgeContinued ?? 0 } : {}) })
|
|
1163
1206
|
: null;
|
|
1164
1207
|
ins.run(
|
|
1165
1208
|
id, st.key, st.nodeId ?? null, st.phase ?? null,
|
|
@@ -1178,6 +1221,8 @@ export async function writeState(pipelineDir, stateObj) {
|
|
|
1178
1221
|
st.trigger === undefined ? null : s(st.trigger),
|
|
1179
1222
|
st.result === undefined ? null : s(st.result),
|
|
1180
1223
|
meta,
|
|
1224
|
+
Number.isFinite(st.humanHours) ? st.humanHours : null,
|
|
1225
|
+
st.humanSignals && typeof st.humanSignals === 'object' ? s(st.humanSignals) : null,
|
|
1181
1226
|
);
|
|
1182
1227
|
}
|
|
1183
1228
|
});
|
|
@@ -1416,10 +1461,13 @@ export function reconcileStaleRunning({
|
|
|
1416
1461
|
.map((r) => r.id);
|
|
1417
1462
|
if (candidates.length === 0) return { reconciled: 0, ids: [] };
|
|
1418
1463
|
|
|
1419
|
-
// Status-guarded UPDATE. Also NULLs owner columns so reclassified rows are clean.
|
|
1464
|
+
// Status-guarded UPDATE. Also NULLs the owner columns so reclassified rows are clean.
|
|
1465
|
+
// heartbeat_at STAYS: it is the last time the dead process was seen alive, and resume()
|
|
1466
|
+
// measures the interrupted run's parked time (and closes its step clocks) from it. Nothing
|
|
1467
|
+
// reads it on a non-running row otherwise; claimPipelineOwnership re-stamps it on resume.
|
|
1420
1468
|
return tx(() => {
|
|
1421
1469
|
const upd = getDb().prepare(
|
|
1422
|
-
`UPDATE pipelines SET status = ?, owner_pid = NULL, owner_host = NULL
|
|
1470
|
+
`UPDATE pipelines SET status = ?, owner_pid = NULL, owner_host = NULL
|
|
1423
1471
|
WHERE id = ? AND status IN (${placeholders})`);
|
|
1424
1472
|
const flipped = [];
|
|
1425
1473
|
for (const id of candidates) {
|
|
@@ -1494,6 +1542,7 @@ function toPipelineRow(o) {
|
|
|
1494
1542
|
updated_at: o.updatedAt ?? null,
|
|
1495
1543
|
total_cost_usd: Number.isFinite(o.totalCostUsd) ? o.totalCostUsd : 0,
|
|
1496
1544
|
total_active_ms: Number.isFinite(o.totalActiveMs) ? o.totalActiveMs : 0,
|
|
1545
|
+
human_hours: Number.isFinite(o.humanHours) ? o.humanHours : 0,
|
|
1497
1546
|
prompt: o.prompt ?? null,
|
|
1498
1547
|
branch: s(o.branch),
|
|
1499
1548
|
workspace_meta: workspaceMeta,
|
|
@@ -1841,6 +1890,8 @@ function stepRowToStep(r) {
|
|
|
1841
1890
|
if (r.execution_id != null && r.cycle != null) step.ordinal = r.cycle; // `ordinal` is the v2 name; `cycle` is its alias
|
|
1842
1891
|
if (r.exec_trigger != null) step.trigger = j(r.exec_trigger, { wireIds: [], freshPorts: [] });
|
|
1843
1892
|
if (r.exec_result != null) step.result = j(r.exec_result, null);
|
|
1893
|
+
if (r.human_hours != null) step.humanHours = r.human_hours;
|
|
1894
|
+
if (r.human_signals != null) step.humanSignals = j(r.human_signals, null);
|
|
1844
1895
|
const em = r.exec_meta != null ? j(r.exec_meta, null) : null;
|
|
1845
1896
|
if (em) {
|
|
1846
1897
|
if (em.taskId != null) step.taskId = em.taskId;
|
|
@@ -1849,6 +1900,10 @@ function stepRowToStep(r) {
|
|
|
1849
1900
|
if (em.phaseOrdinal != null) step.phaseOrdinal = em.phaseOrdinal;
|
|
1850
1901
|
if (em.taskIndex != null) step.taskIndex = em.taskIndex;
|
|
1851
1902
|
if (em.taskTotal != null) step.taskTotal = em.taskTotal;
|
|
1903
|
+
if (em.nodeKey != null) step.nodeKey = em.nodeKey;
|
|
1904
|
+
if (em.runtime != null) step.runtime = em.runtime;
|
|
1905
|
+
if (em.exitCode != null) step.exitCode = em.exitCode;
|
|
1906
|
+
if (em.bridgeCalls != null) { step.bridgeCalls = em.bridgeCalls; step.bridgeContinued = em.bridgeContinued ?? 0; }
|
|
1852
1907
|
}
|
|
1853
1908
|
return step;
|
|
1854
1909
|
}
|
|
@@ -1875,6 +1930,7 @@ function rowToState(row) {
|
|
|
1875
1930
|
updatedAt: row.updated_at ?? null,
|
|
1876
1931
|
totalCostUsd: row.total_cost_usd ?? 0,
|
|
1877
1932
|
totalActiveMs: row.total_active_ms ?? 0,
|
|
1933
|
+
humanHours: Number(row.human_hours || 0),
|
|
1878
1934
|
prompt: row.prompt ?? null,
|
|
1879
1935
|
baseName: row.base_name ?? null,
|
|
1880
1936
|
datePrefix: row.date_prefix ?? null,
|
|
@@ -1882,13 +1938,17 @@ function rowToState(row) {
|
|
|
1882
1938
|
stepper: j(row.stepper, null),
|
|
1883
1939
|
tools: j(row.tools, null),
|
|
1884
1940
|
guardrailsId: row.guardrails_id ?? null,
|
|
1941
|
+
// v31 provenance: set when a schedule started this run (NULL = started by hand).
|
|
1942
|
+
scheduledFor: row.scheduled_for ?? null,
|
|
1943
|
+
scheduleId: row.schedule_id ?? null,
|
|
1885
1944
|
// A retired v1 resume point was NULLed by the v2 upgrade: the run stays in
|
|
1886
1945
|
// History with an honest status, but it can never be resumed again.
|
|
1887
1946
|
resumable: row.resume_point != null,
|
|
1888
1947
|
steps: getDb().prepare(`
|
|
1889
1948
|
SELECT key, node_id, phase, step_index, cycle, status, started_at, updated_at,
|
|
1890
1949
|
active_ms, running_since, cost_usd, session_id, skills, graphify_count,
|
|
1891
|
-
execution_id, exec_kind, agent_key, ended_at, exec_trigger, exec_result, exec_meta
|
|
1950
|
+
execution_id, exec_kind, agent_key, ended_at, exec_trigger, exec_result, exec_meta,
|
|
1951
|
+
human_hours, human_signals
|
|
1892
1952
|
FROM pipeline_steps WHERE pipeline_id = ? ORDER BY rowid
|
|
1893
1953
|
`).all(row.id).map(stepRowToStep),
|
|
1894
1954
|
subAgents: listSubAgents(row.id),
|
|
@@ -2161,6 +2221,9 @@ export async function readPipelineByKey(key, id) {
|
|
|
2161
2221
|
results,
|
|
2162
2222
|
overview,
|
|
2163
2223
|
teamMetrics: readRunLedger(row.id),
|
|
2224
|
+
// Team policy (design §10 / board 10): the home the run's policy came from and what the
|
|
2225
|
+
// developer did about it — the History meta line's "policy · 1 override" segment.
|
|
2226
|
+
policy: readPolicyState(row.id),
|
|
2164
2227
|
memory: await readMemoryLedger(dir),
|
|
2165
2228
|
...readPipelineExtras(row.id),
|
|
2166
2229
|
};
|