@worca/app 1.0.0 → 1.1.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 +22 -9
- package/agents/clarify.meta.json +4 -4
- package/agents/decomposer.meta.json +5 -5
- package/agents/implementer.meta.json +15 -5
- package/agents/manualTestsChecklist.meta.json +5 -4
- package/agents/manualWebUiTesting.meta.json +9 -4
- package/agents/planReviewer.meta.json +12 -4
- package/agents/planner.meta.json +12 -5
- package/agents/refiner.meta.json +15 -4
- package/agents/reviewer.meta.json +14 -4
- package/agents/worca-cc-clarify.md +7 -0
- package/agents/worca-cc-code-reviewer.md +11 -6
- package/agents/worca-cc-decomposer.md +7 -0
- package/agents/worca-cc-implementer.md +9 -0
- package/agents/worca-cc-manual-tests-checklist.md +8 -5
- package/agents/worca-cc-manual-web-ui-testing.md +10 -6
- package/agents/worca-cc-plan-refiner.md +11 -6
- package/agents/worca-cc-plan-reviewer.md +10 -7
- package/agents/worca-cc-planner.md +9 -0
- package/agents/worca-cc-workspace-reviewer.md +11 -4
- package/agents/worca-cc-workspace-scanner.md +8 -4
- package/agents/workspaceReviewer.meta.json +15 -4
- package/agents/workspaceScanner.meta.json +5 -4
- package/package.json +8 -2
- package/skills/worca/SKILL.md +5 -5
- package/src/cli/render.mjs +148 -0
- package/src/cli/worca-cc.mjs +319 -45
- package/src/core/agent-gen.mjs +69 -31
- package/src/core/agent-registry.mjs +124 -144
- package/src/core/agent-store.mjs +164 -4
- package/src/core/artifacts.mjs +189 -21
- package/src/core/ask/catalog.mjs +111 -0
- package/src/core/ask/comment-deps.mjs +55 -0
- package/src/core/ask/events.mjs +506 -0
- package/src/core/ask/follow.mjs +107 -0
- package/src/core/ask/git-allowlist.mjs +226 -0
- package/src/core/ask/limits.mjs +54 -0
- package/src/core/ask/mcp-stdio.mjs +135 -0
- package/src/core/ask/models.mjs +125 -0
- package/src/core/ask/prompt.mjs +261 -0
- package/src/core/ask/proposal.mjs +170 -0
- package/src/core/ask/redact.mjs +30 -0
- package/src/core/ask/spawn.mjs +153 -0
- package/src/core/ask/store.mjs +360 -0
- package/src/core/ask/tool-deps.mjs +63 -0
- package/src/core/ask/tools.mjs +848 -0
- package/src/core/ask/turn.mjs +416 -0
- package/src/core/ask/worktree-deps.mjs +27 -0
- package/src/core/ask/worktrees.mjs +285 -0
- package/src/core/chat/command-router.mjs +20 -3
- package/src/core/claude-runner.mjs +434 -57
- package/src/core/config.mjs +264 -41
- package/src/core/cost-budget.mjs +29 -2
- package/src/core/db.mjs +684 -47
- package/src/core/diff-anchor.mjs +213 -0
- package/src/core/diff-comments.mjs +273 -0
- package/src/core/engine-select.mjs +32 -0
- package/src/core/git-info.mjs +49 -10
- package/src/core/graph/builtin-workflows.mjs +51 -0
- package/src/core/graph/executor.mjs +894 -0
- package/src/core/graph/registry-ports.mjs +12 -0
- package/src/core/graph/scheduler.mjs +1065 -0
- package/src/core/graph/seed-templates.mjs +318 -0
- package/src/core/model-env.mjs +112 -8
- package/src/core/model-test.mjs +79 -0
- package/src/core/orchestrator.mjs +902 -4098
- package/src/core/overview-agent.mjs +15 -3
- package/src/core/phases.mjs +208 -537
- package/src/core/pipeline-delete.mjs +13 -2
- package/src/core/plugin-api.mjs +8 -3
- package/src/core/plugin-config.mjs +178 -28
- package/src/core/plugin-inventory.mjs +6 -2
- package/src/core/plugin-manifest.mjs +199 -11
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-repo.mjs +16 -4
- package/src/core/plugin-shim-child.mjs +9 -3
- package/src/core/plugin-shim.mjs +77 -14
- package/src/core/plugin-store.mjs +236 -29
- package/src/core/plugin-workflows.mjs +90 -41
- package/src/core/preflight.mjs +135 -3
- package/src/core/projects.mjs +7 -5
- package/src/core/protocol.mjs +8 -35
- package/src/core/recoverable-error.mjs +1 -1
- package/src/core/run-harness.mjs +3585 -0
- package/src/core/run-manifest.mjs +5 -1
- package/src/core/settings.mjs +109 -13
- package/src/core/skills.mjs +10 -3
- package/src/core/source-bindings.mjs +175 -0
- package/src/core/sources.mjs +87 -25
- package/src/core/stats.mjs +25 -6
- package/src/core/title.mjs +51 -4
- package/src/core/workflows.mjs +358 -259
- package/src/core/workspace-scan.mjs +4 -0
- package/src/core/worktree.mjs +98 -7
- package/src/shared/graph/agent-meta.mjs +278 -0
- package/src/shared/graph/constants.mjs +105 -0
- package/src/shared/graph/geometry.mjs +157 -0
- package/src/shared/graph/layout.mjs +134 -0
- package/src/shared/graph/loops.mjs +130 -0
- package/src/shared/graph/manifest.mjs +257 -0
- package/src/shared/graph/ports.mjs +153 -0
- package/src/shared/graph/route.mjs +397 -0
- package/src/shared/graph/template.mjs +165 -0
- package/src/shared/graph/thumbnail.mjs +67 -0
- package/src/shared/graph/validate.mjs +491 -0
- package/src/shared/graph/verdict.mjs +41 -0
- package/ui/public/app.js +4008 -1670
- package/ui/public/ask-markdown.mjs +145 -0
- package/ui/public/ask-model.mjs +264 -0
- package/ui/public/ask-panel.mjs +1880 -0
- package/ui/public/chat-settings-view.mjs +6 -2
- package/ui/public/diff-view.mjs +66 -11
- package/ui/public/file-tree.mjs +305 -0
- package/ui/public/graph/composer.mjs +889 -0
- package/ui/public/graph/inspector.mjs +183 -0
- package/ui/public/graph/model.mjs +37 -0
- package/ui/public/graph/palette.mjs +144 -0
- package/ui/public/graph/run-decor.mjs +410 -0
- package/ui/public/graph/run-hosts.mjs +201 -0
- package/ui/public/graph/save-dialog.mjs +56 -0
- package/ui/public/graph/view.mjs +858 -0
- package/ui/public/guardrails-view.mjs +4 -2
- package/ui/public/hljs-loader.mjs +180 -0
- package/ui/public/index.html +269 -265
- package/ui/public/log-filter.mjs +22 -4
- package/ui/public/log-line.mjs +45 -19
- package/ui/public/models-view.mjs +171 -9
- package/ui/public/plugins-view.mjs +106 -4
- package/ui/public/source-pane.mjs +190 -8
- package/ui/public/stats-view.mjs +81 -1
- package/ui/public/style.css +1459 -229
- package/ui/public/syntax-highlight.mjs +270 -0
- package/ui/public/thinking-orb.mjs +110 -0
- package/ui/server.mjs +1667 -98
- package/src/core/channels.mjs +0 -302
- package/src/core/runners.mjs +0 -167
- package/src/core/workflow-validator.mjs +0 -185
- package/ui/public/composer-core.mjs +0 -211
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// src/core/graph/seed-templates.mjs
|
|
2
|
+
// The 7 hand-written v2 seed templates the V17 migration re-seeds the user's
|
|
3
|
+
// saved pipelines as, plus the static overlay-migration maps.
|
|
4
|
+
//
|
|
5
|
+
// Each entry is a FLAT Template object — `{ id, name, version, domain, createdAt,
|
|
6
|
+
// nodes, wires }` at the TOP level, never nested under `graph`. Flat is
|
|
7
|
+
// load-bearing: validateGraph runs on the whole template and V1 reads `version`
|
|
8
|
+
// off the object it is handed.
|
|
9
|
+
//
|
|
10
|
+
// Shape rules the graphs follow (a copy-check, not an authoring recipe — these
|
|
11
|
+
// were hand-authored and verified against the v1 rows):
|
|
12
|
+
// - Every template has a Task source node and exactly ONE End node, wired from
|
|
13
|
+
// `webui.pass` where a webui node exists, else `reviewer.pass`.
|
|
14
|
+
// - `implementer.done -> reviewer.done`, `reviewer.pass -> checklist.await` and
|
|
15
|
+
// `checklist.checklist -> webui.checklist` reproduce v1's linear order; refine
|
|
16
|
+
// self-loops and review loops carry `config.maxCycles: 3`.
|
|
17
|
+
// - The double-loop templates (wf_full, wf_provided-plan, wf_full-no-decompose)
|
|
18
|
+
// fan `reviewer.review -> n_or.in1` and `webui.review -> n_or.in2` — each
|
|
19
|
+
// in-wire keeps its OWN maxCycles, because those are the loop wires and the
|
|
20
|
+
// gate sites — while `n_or.out -> implementer.fix` carries NO config
|
|
21
|
+
// (maxCycles on an always-sourced wire fails V13). wf_no-clarify has no or
|
|
22
|
+
// card: only w10 targets `fix`, and `webui.review` is UNWIRED, matching its
|
|
23
|
+
// two-feedback v1 row.
|
|
24
|
+
//
|
|
25
|
+
// Parity note: the OR in-wires keep their v1-era wire ids and budgets, so source
|
|
26
|
+
// firings, per-wire delivery counts and gate sites are identical to the direct
|
|
27
|
+
// wire shape; the OR adds one $0 instant exec row per loop emission and forwards
|
|
28
|
+
// the identical review path to `fix`.
|
|
29
|
+
|
|
30
|
+
import { deepFreeze } from './builtin-workflows.mjs';
|
|
31
|
+
|
|
32
|
+
/** Full: clarify -> plan -> refine -> decompose -> implement -> review ->
|
|
33
|
+
* checklist -> web UI, with both review loops fanned through the or card.
|
|
34
|
+
* 11 nodes / 17 wires. */
|
|
35
|
+
const WF_FULL = {
|
|
36
|
+
id: 'wf_full',
|
|
37
|
+
name: 'Full',
|
|
38
|
+
version: 2,
|
|
39
|
+
domain: 'coding',
|
|
40
|
+
createdAt: '2026-07-29T19:39:27.650Z',
|
|
41
|
+
nodes: [
|
|
42
|
+
{ id: 'n_task', kind: 'task', x: 60, y: 198, config: {} },
|
|
43
|
+
{ id: 'n_clarify', kind: 'agent', key: 'clarify', x: 360, y: 198, config: {} },
|
|
44
|
+
{ id: 'n_plan', kind: 'agent', key: 'planner', x: 660, y: 198, config: {} },
|
|
45
|
+
{ id: 'n_refine', kind: 'agent', key: 'refiner', x: 960, y: 198, config: {} },
|
|
46
|
+
{ id: 'n_decompose', kind: 'agent', key: 'decomposer', x: 1260, y: 198, config: {} },
|
|
47
|
+
{ id: 'n_impl', kind: 'agent', key: 'implementer', x: 1560, y: 198, config: {} },
|
|
48
|
+
{ id: 'n_review', kind: 'agent', key: 'reviewer', x: 1860, y: 198, config: {} },
|
|
49
|
+
{ id: 'n_check', kind: 'agent', key: 'manualTestsChecklist', x: 2160, y: 198, config: {} },
|
|
50
|
+
{ id: 'n_webui', kind: 'agent', key: 'manualWebUiTesting', x: 2460, y: 198, config: {} },
|
|
51
|
+
{ id: 'n_end', kind: 'end', x: 2760, y: 198, config: {} },
|
|
52
|
+
{ id: 'n_or', kind: 'or', x: 2010, y: 430, config: { arity: 2 } },
|
|
53
|
+
],
|
|
54
|
+
wires: [
|
|
55
|
+
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_clarify', port: 'task' } },
|
|
56
|
+
{ id: 'w2', from: { node: 'n_task', port: 'task' }, to: { node: 'n_plan', port: 'task' } },
|
|
57
|
+
{ id: 'w3', from: { node: 'n_clarify', port: 'answers' }, to: { node: 'n_plan', port: 'answers' } },
|
|
58
|
+
{ id: 'w4', from: { node: 'n_plan', port: 'plan' }, to: { node: 'n_refine', port: 'plan' } },
|
|
59
|
+
{ id: 'w5', from: { node: 'n_refine', port: 'revise' }, to: { node: 'n_refine', port: 'revise' }, config: { maxCycles: 3 } },
|
|
60
|
+
{ id: 'w6', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_decompose', port: 'plan' } },
|
|
61
|
+
{ id: 'w7', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_impl', port: 'plan' } },
|
|
62
|
+
{ id: 'w8', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_review', port: 'plan' } },
|
|
63
|
+
{ id: 'w9', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_check', port: 'plan' } },
|
|
64
|
+
{ id: 'w10', from: { node: 'n_decompose', port: 'tasks' }, to: { node: 'n_impl', port: 'task' } },
|
|
65
|
+
{ id: 'w11', from: { node: 'n_impl', port: 'done' }, to: { node: 'n_review', port: 'done' } },
|
|
66
|
+
{ id: 'w12', from: { node: 'n_review', port: 'review' }, to: { node: 'n_or', port: 'in1' }, config: { maxCycles: 3 } },
|
|
67
|
+
{ id: 'w13', from: { node: 'n_review', port: 'pass' }, to: { node: 'n_check', port: 'await' } },
|
|
68
|
+
{ id: 'w14', from: { node: 'n_check', port: 'checklist' }, to: { node: 'n_webui', port: 'checklist' } },
|
|
69
|
+
{ id: 'w15', from: { node: 'n_webui', port: 'review' }, to: { node: 'n_or', port: 'in2' }, config: { maxCycles: 3 } },
|
|
70
|
+
{ id: 'w16', from: { node: 'n_webui', port: 'pass' }, to: { node: 'n_end', port: 'result' } },
|
|
71
|
+
{ id: 'w17', from: { node: 'n_or', port: 'out' }, to: { node: 'n_impl', port: 'fix' } },
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** No Clarify: Full minus the clarify node, and with only ONE feedback into
|
|
76
|
+
* `fix` — w10 reviewer.review. `webui.review` is deliberately unwired, matching
|
|
77
|
+
* the v1 row's two feedbacks. 9 nodes / 13 wires. */
|
|
78
|
+
const WF_NO_CLARIFY = {
|
|
79
|
+
id: 'wf_no-clarify',
|
|
80
|
+
name: 'No Clarify',
|
|
81
|
+
version: 2,
|
|
82
|
+
domain: 'coding',
|
|
83
|
+
createdAt: '2026-07-29T19:40:22.212Z',
|
|
84
|
+
nodes: [
|
|
85
|
+
{ id: 'n_task', kind: 'task', x: 60, y: 198, config: {} },
|
|
86
|
+
{ id: 'n_plan', kind: 'agent', key: 'planner', x: 360, y: 198, config: {} },
|
|
87
|
+
{ id: 'n_refine', kind: 'agent', key: 'refiner', x: 660, y: 198, config: {} },
|
|
88
|
+
{ id: 'n_decompose', kind: 'agent', key: 'decomposer', x: 960, y: 198, config: {} },
|
|
89
|
+
{ id: 'n_impl', kind: 'agent', key: 'implementer', x: 1260, y: 198, config: {} },
|
|
90
|
+
{ id: 'n_review', kind: 'agent', key: 'reviewer', x: 1560, y: 198, config: {} },
|
|
91
|
+
{ id: 'n_check', kind: 'agent', key: 'manualTestsChecklist', x: 1860, y: 198, config: {} },
|
|
92
|
+
{ id: 'n_webui', kind: 'agent', key: 'manualWebUiTesting', x: 2160, y: 198, config: {} },
|
|
93
|
+
{ id: 'n_end', kind: 'end', x: 2460, y: 198, config: {} },
|
|
94
|
+
],
|
|
95
|
+
wires: [
|
|
96
|
+
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_plan', port: 'task' } },
|
|
97
|
+
{ id: 'w2', from: { node: 'n_plan', port: 'plan' }, to: { node: 'n_refine', port: 'plan' } },
|
|
98
|
+
{ id: 'w3', from: { node: 'n_refine', port: 'revise' }, to: { node: 'n_refine', port: 'revise' }, config: { maxCycles: 3 } },
|
|
99
|
+
{ id: 'w4', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_decompose', port: 'plan' } },
|
|
100
|
+
{ id: 'w5', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_impl', port: 'plan' } },
|
|
101
|
+
{ id: 'w6', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_review', port: 'plan' } },
|
|
102
|
+
{ id: 'w7', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_check', port: 'plan' } },
|
|
103
|
+
{ id: 'w8', from: { node: 'n_decompose', port: 'tasks' }, to: { node: 'n_impl', port: 'task' } },
|
|
104
|
+
{ id: 'w9', from: { node: 'n_impl', port: 'done' }, to: { node: 'n_review', port: 'done' } },
|
|
105
|
+
{ id: 'w10', from: { node: 'n_review', port: 'review' }, to: { node: 'n_impl', port: 'fix' }, config: { maxCycles: 3 } },
|
|
106
|
+
{ id: 'w11', from: { node: 'n_review', port: 'pass' }, to: { node: 'n_check', port: 'await' } },
|
|
107
|
+
{ id: 'w12', from: { node: 'n_check', port: 'checklist' }, to: { node: 'n_webui', port: 'checklist' } },
|
|
108
|
+
{ id: 'w13', from: { node: 'n_webui', port: 'pass' }, to: { node: 'n_end', port: 'result' } },
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Provided Plan: no planner — the task node carries `planStoreSeed` (A2) and
|
|
113
|
+
* wires straight into `refiner.plan`. 9 nodes / 14 wires. */
|
|
114
|
+
const WF_PROVIDED_PLAN = {
|
|
115
|
+
id: 'wf_provided-plan',
|
|
116
|
+
name: 'Provided Plan',
|
|
117
|
+
version: 2,
|
|
118
|
+
domain: 'coding',
|
|
119
|
+
createdAt: '2026-08-07T11:29:56.074Z',
|
|
120
|
+
nodes: [
|
|
121
|
+
{ id: 'n_task', kind: 'task', x: 60, y: 198, config: { planStoreSeed: true } },
|
|
122
|
+
{ id: 'n_refine', kind: 'agent', key: 'refiner', x: 360, y: 198, config: {} },
|
|
123
|
+
{ id: 'n_decompose', kind: 'agent', key: 'decomposer', x: 660, y: 198, config: {} },
|
|
124
|
+
{ id: 'n_impl', kind: 'agent', key: 'implementer', x: 960, y: 198, config: {} },
|
|
125
|
+
{ id: 'n_review', kind: 'agent', key: 'reviewer', x: 1260, y: 198, config: {} },
|
|
126
|
+
{ id: 'n_check', kind: 'agent', key: 'manualTestsChecklist', x: 1560, y: 198, config: {} },
|
|
127
|
+
{ id: 'n_webui', kind: 'agent', key: 'manualWebUiTesting', x: 1860, y: 198, config: {} },
|
|
128
|
+
{ id: 'n_end', kind: 'end', x: 2160, y: 198, config: {} },
|
|
129
|
+
{ id: 'n_or', kind: 'or', x: 1410, y: 430, config: { arity: 2 } },
|
|
130
|
+
],
|
|
131
|
+
wires: [
|
|
132
|
+
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_refine', port: 'plan' } },
|
|
133
|
+
{ id: 'w2', from: { node: 'n_refine', port: 'revise' }, to: { node: 'n_refine', port: 'revise' }, config: { maxCycles: 3 } },
|
|
134
|
+
{ id: 'w3', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_decompose', port: 'plan' } },
|
|
135
|
+
{ id: 'w4', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_impl', port: 'plan' } },
|
|
136
|
+
{ id: 'w5', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_review', port: 'plan' } },
|
|
137
|
+
{ id: 'w6', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_check', port: 'plan' } },
|
|
138
|
+
{ id: 'w7', from: { node: 'n_decompose', port: 'tasks' }, to: { node: 'n_impl', port: 'task' } },
|
|
139
|
+
{ id: 'w8', from: { node: 'n_impl', port: 'done' }, to: { node: 'n_review', port: 'done' } },
|
|
140
|
+
{ id: 'w9', from: { node: 'n_review', port: 'review' }, to: { node: 'n_or', port: 'in1' }, config: { maxCycles: 3 } },
|
|
141
|
+
{ id: 'w10', from: { node: 'n_review', port: 'pass' }, to: { node: 'n_check', port: 'await' } },
|
|
142
|
+
{ id: 'w11', from: { node: 'n_check', port: 'checklist' }, to: { node: 'n_webui', port: 'checklist' } },
|
|
143
|
+
{ id: 'w12', from: { node: 'n_webui', port: 'review' }, to: { node: 'n_or', port: 'in2' }, config: { maxCycles: 3 } },
|
|
144
|
+
{ id: 'w13', from: { node: 'n_webui', port: 'pass' }, to: { node: 'n_end', port: 'result' } },
|
|
145
|
+
{ id: 'w14', from: { node: 'n_or', port: 'out' }, to: { node: 'n_impl', port: 'fix' } },
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** FULL-NO-Decompose: Full minus the decomposer, so the implementer takes the
|
|
150
|
+
* whole plan. 10 nodes / 15 wires. */
|
|
151
|
+
const WF_FULL_NO_DECOMPOSE = {
|
|
152
|
+
id: 'wf_full-no-decompose',
|
|
153
|
+
name: 'FULL-NO-Decompose',
|
|
154
|
+
version: 2,
|
|
155
|
+
domain: 'coding',
|
|
156
|
+
createdAt: '2026-08-08T00:02:32.776Z',
|
|
157
|
+
nodes: [
|
|
158
|
+
{ id: 'n_task', kind: 'task', x: 60, y: 198, config: {} },
|
|
159
|
+
{ id: 'n_clarify', kind: 'agent', key: 'clarify', x: 360, y: 198, config: {} },
|
|
160
|
+
{ id: 'n_plan', kind: 'agent', key: 'planner', x: 660, y: 198, config: {} },
|
|
161
|
+
{ id: 'n_refine', kind: 'agent', key: 'refiner', x: 960, y: 198, config: {} },
|
|
162
|
+
{ id: 'n_impl', kind: 'agent', key: 'implementer', x: 1260, y: 198, config: {} },
|
|
163
|
+
{ id: 'n_review', kind: 'agent', key: 'reviewer', x: 1560, y: 198, config: {} },
|
|
164
|
+
{ id: 'n_check', kind: 'agent', key: 'manualTestsChecklist', x: 1860, y: 198, config: {} },
|
|
165
|
+
{ id: 'n_webui', kind: 'agent', key: 'manualWebUiTesting', x: 2160, y: 198, config: {} },
|
|
166
|
+
{ id: 'n_end', kind: 'end', x: 2460, y: 198, config: {} },
|
|
167
|
+
{ id: 'n_or', kind: 'or', x: 1710, y: 430, config: { arity: 2 } },
|
|
168
|
+
],
|
|
169
|
+
wires: [
|
|
170
|
+
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_clarify', port: 'task' } },
|
|
171
|
+
{ id: 'w2', from: { node: 'n_task', port: 'task' }, to: { node: 'n_plan', port: 'task' } },
|
|
172
|
+
{ id: 'w3', from: { node: 'n_clarify', port: 'answers' }, to: { node: 'n_plan', port: 'answers' } },
|
|
173
|
+
{ id: 'w4', from: { node: 'n_plan', port: 'plan' }, to: { node: 'n_refine', port: 'plan' } },
|
|
174
|
+
{ id: 'w5', from: { node: 'n_refine', port: 'revise' }, to: { node: 'n_refine', port: 'revise' }, config: { maxCycles: 3 } },
|
|
175
|
+
{ id: 'w6', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_impl', port: 'plan' } },
|
|
176
|
+
{ id: 'w7', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_review', port: 'plan' } },
|
|
177
|
+
{ id: 'w8', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_check', port: 'plan' } },
|
|
178
|
+
{ id: 'w9', from: { node: 'n_impl', port: 'done' }, to: { node: 'n_review', port: 'done' } },
|
|
179
|
+
{ id: 'w10', from: { node: 'n_review', port: 'review' }, to: { node: 'n_or', port: 'in1' }, config: { maxCycles: 3 } },
|
|
180
|
+
{ id: 'w11', from: { node: 'n_review', port: 'pass' }, to: { node: 'n_check', port: 'await' } },
|
|
181
|
+
{ id: 'w12', from: { node: 'n_check', port: 'checklist' }, to: { node: 'n_webui', port: 'checklist' } },
|
|
182
|
+
{ id: 'w13', from: { node: 'n_webui', port: 'review' }, to: { node: 'n_or', port: 'in2' }, config: { maxCycles: 3 } },
|
|
183
|
+
{ id: 'w14', from: { node: 'n_webui', port: 'pass' }, to: { node: 'n_end', port: 'result' } },
|
|
184
|
+
{ id: 'w15', from: { node: 'n_or', port: 'out' }, to: { node: 'n_impl', port: 'fix' } },
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** Quick Fix: plan -> implement -> review, no refiner, no checklist. The
|
|
189
|
+
* reviewer's only other input is the VOID `done` port, which is why it stays
|
|
190
|
+
* warning-free under V18 (exemption (b)). 5 nodes / 6 wires. */
|
|
191
|
+
const WF_QUICK_FIX = {
|
|
192
|
+
id: 'wf_quick-fix',
|
|
193
|
+
name: 'Quick Fix',
|
|
194
|
+
version: 2,
|
|
195
|
+
domain: 'coding',
|
|
196
|
+
createdAt: '2026-08-09T14:40:59.262Z',
|
|
197
|
+
nodes: [
|
|
198
|
+
{ id: 'n_task', kind: 'task', x: 60, y: 198, config: {} },
|
|
199
|
+
{ id: 'n_plan', kind: 'agent', key: 'planner', x: 360, y: 198, config: {} },
|
|
200
|
+
{ id: 'n_impl', kind: 'agent', key: 'implementer', x: 660, y: 198, config: {} },
|
|
201
|
+
{ id: 'n_review', kind: 'agent', key: 'reviewer', x: 960, y: 198, config: {} },
|
|
202
|
+
{ id: 'n_end', kind: 'end', x: 1260, y: 198, config: {} },
|
|
203
|
+
],
|
|
204
|
+
wires: [
|
|
205
|
+
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_plan', port: 'task' } },
|
|
206
|
+
{ id: 'w2', from: { node: 'n_plan', port: 'plan' }, to: { node: 'n_impl', port: 'plan' } },
|
|
207
|
+
{ id: 'w3', from: { node: 'n_plan', port: 'plan' }, to: { node: 'n_review', port: 'plan' } },
|
|
208
|
+
{ id: 'w4', from: { node: 'n_impl', port: 'done' }, to: { node: 'n_review', port: 'done' } },
|
|
209
|
+
{ id: 'w5', from: { node: 'n_review', port: 'review' }, to: { node: 'n_impl', port: 'fix' }, config: { maxCycles: 3 } },
|
|
210
|
+
{ id: 'w6', from: { node: 'n_review', port: 'pass' }, to: { node: 'n_end', port: 'result' } },
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Clarify -> Implement: clarify -> plan -> refine -> implement -> review.
|
|
215
|
+
* 7 nodes / 10 wires. */
|
|
216
|
+
const WF_CLARIFY_IMPLEMENT = {
|
|
217
|
+
id: 'wf_clarify-implement',
|
|
218
|
+
name: 'Clarify -> Implement',
|
|
219
|
+
version: 2,
|
|
220
|
+
domain: 'coding',
|
|
221
|
+
createdAt: '2026-08-09T15:16:43.806Z',
|
|
222
|
+
nodes: [
|
|
223
|
+
{ id: 'n_task', kind: 'task', x: 60, y: 198, config: {} },
|
|
224
|
+
{ id: 'n_clarify', kind: 'agent', key: 'clarify', x: 360, y: 198, config: {} },
|
|
225
|
+
{ id: 'n_plan', kind: 'agent', key: 'planner', x: 660, y: 198, config: {} },
|
|
226
|
+
{ id: 'n_refine', kind: 'agent', key: 'refiner', x: 960, y: 198, config: {} },
|
|
227
|
+
{ id: 'n_impl', kind: 'agent', key: 'implementer', x: 1260, y: 198, config: {} },
|
|
228
|
+
{ id: 'n_review', kind: 'agent', key: 'reviewer', x: 1560, y: 198, config: {} },
|
|
229
|
+
{ id: 'n_end', kind: 'end', x: 1860, y: 198, config: {} },
|
|
230
|
+
],
|
|
231
|
+
wires: [
|
|
232
|
+
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_clarify', port: 'task' } },
|
|
233
|
+
{ id: 'w2', from: { node: 'n_task', port: 'task' }, to: { node: 'n_plan', port: 'task' } },
|
|
234
|
+
{ id: 'w3', from: { node: 'n_clarify', port: 'answers' }, to: { node: 'n_plan', port: 'answers' } },
|
|
235
|
+
{ id: 'w4', from: { node: 'n_plan', port: 'plan' }, to: { node: 'n_refine', port: 'plan' } },
|
|
236
|
+
{ id: 'w5', from: { node: 'n_refine', port: 'revise' }, to: { node: 'n_refine', port: 'revise' }, config: { maxCycles: 3 } },
|
|
237
|
+
{ id: 'w6', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_impl', port: 'plan' } },
|
|
238
|
+
{ id: 'w7', from: { node: 'n_refine', port: 'plan' }, to: { node: 'n_review', port: 'plan' } },
|
|
239
|
+
{ id: 'w8', from: { node: 'n_impl', port: 'done' }, to: { node: 'n_review', port: 'done' } },
|
|
240
|
+
{ id: 'w9', from: { node: 'n_review', port: 'review' }, to: { node: 'n_impl', port: 'fix' }, config: { maxCycles: 3 } },
|
|
241
|
+
{ id: 'w10', from: { node: 'n_review', port: 'pass' }, to: { node: 'n_end', port: 'result' } },
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** Clarify -> Quick Fix: Quick Fix with a clarify step in front. 6 nodes /
|
|
246
|
+
* 8 wires. */
|
|
247
|
+
const WF_CLARIFY_QUICK_FIX = {
|
|
248
|
+
id: 'wf_clarify-quick-fix',
|
|
249
|
+
name: 'Clarify -> Quick Fix',
|
|
250
|
+
version: 2,
|
|
251
|
+
domain: 'coding',
|
|
252
|
+
createdAt: '2026-08-09T15:18:40.077Z',
|
|
253
|
+
nodes: [
|
|
254
|
+
{ id: 'n_task', kind: 'task', x: 60, y: 198, config: {} },
|
|
255
|
+
{ id: 'n_clarify', kind: 'agent', key: 'clarify', x: 360, y: 198, config: {} },
|
|
256
|
+
{ id: 'n_plan', kind: 'agent', key: 'planner', x: 660, y: 198, config: {} },
|
|
257
|
+
{ id: 'n_impl', kind: 'agent', key: 'implementer', x: 960, y: 198, config: {} },
|
|
258
|
+
{ id: 'n_review', kind: 'agent', key: 'reviewer', x: 1260, y: 198, config: {} },
|
|
259
|
+
{ id: 'n_end', kind: 'end', x: 1560, y: 198, config: {} },
|
|
260
|
+
],
|
|
261
|
+
wires: [
|
|
262
|
+
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_clarify', port: 'task' } },
|
|
263
|
+
{ id: 'w2', from: { node: 'n_task', port: 'task' }, to: { node: 'n_plan', port: 'task' } },
|
|
264
|
+
{ id: 'w3', from: { node: 'n_clarify', port: 'answers' }, to: { node: 'n_plan', port: 'answers' } },
|
|
265
|
+
{ id: 'w4', from: { node: 'n_plan', port: 'plan' }, to: { node: 'n_impl', port: 'plan' } },
|
|
266
|
+
{ id: 'w5', from: { node: 'n_plan', port: 'plan' }, to: { node: 'n_review', port: 'plan' } },
|
|
267
|
+
{ id: 'w6', from: { node: 'n_impl', port: 'done' }, to: { node: 'n_review', port: 'done' } },
|
|
268
|
+
{ id: 'w7', from: { node: 'n_review', port: 'review' }, to: { node: 'n_impl', port: 'fix' }, config: { maxCycles: 3 } },
|
|
269
|
+
{ id: 'w8', from: { node: 'n_review', port: 'pass' }, to: { node: 'n_end', port: 'result' } },
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/** The 7 seeds, in seeding order. */
|
|
274
|
+
export const SEED_TEMPLATES = deepFreeze([
|
|
275
|
+
WF_FULL,
|
|
276
|
+
WF_NO_CLARIFY,
|
|
277
|
+
WF_PROVIDED_PLAN,
|
|
278
|
+
WF_FULL_NO_DECOMPOSE,
|
|
279
|
+
WF_QUICK_FIX,
|
|
280
|
+
WF_CLARIFY_IMPLEMENT,
|
|
281
|
+
WF_CLARIFY_QUICK_FIX,
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
/** V17 overlay migration: `config_workflow_nodes.node_id` rewrites, old v1 step
|
|
285
|
+
* id -> v2 node id. wf_default is included for projects carrying overlays on
|
|
286
|
+
* the builtin default workflow. */
|
|
287
|
+
export const NODE_ID_MAP = deepFreeze({
|
|
288
|
+
wf_full: { s0_0: 'n_clarify', s1_0: 'n_plan', s2_0: 'n_refine', s3_0: 'n_decompose', s4_0: 'n_impl', s5_0: 'n_review', s6_0: 'n_check', s7_0: 'n_webui' },
|
|
289
|
+
'wf_no-clarify': { s0_0: 'n_plan', s1_0: 'n_refine', s2_0: 'n_decompose', s3_0: 'n_impl', s4_0: 'n_review', s5_0: 'n_check', s6_0: 'n_webui' },
|
|
290
|
+
'wf_provided-plan': { s0_0: 'n_refine', s1_0: 'n_decompose', s2_0: 'n_impl', s3_0: 'n_review', s4_0: 'n_check', s5_0: 'n_webui' },
|
|
291
|
+
'wf_full-no-decompose': { s0_0: 'n_clarify', s1_0: 'n_plan', s2_0: 'n_refine', s3_0: 'n_impl', s4_0: 'n_review', s5_0: 'n_check', s6_0: 'n_webui' },
|
|
292
|
+
'wf_quick-fix': { s0_0: 'n_plan', s1_0: 'n_impl', s2_0: 'n_review' },
|
|
293
|
+
'wf_clarify-implement': { s0_0: 'n_clarify', s1_0: 'n_plan', s2_0: 'n_refine', s3_0: 'n_impl', s4_0: 'n_review' },
|
|
294
|
+
'wf_clarify-quick-fix': { s0_0: 'n_clarify', s1_0: 'n_plan', s2_0: 'n_impl', s3_0: 'n_review' },
|
|
295
|
+
wf_default: { s_clarify: 'n_clarify', s0_0: 'n_plan', s1_0: 'n_refine', s2_0: 'n_impl', s3_0: 'n_review' },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
/** V17 overlay migration: `config_workflow_feedbacks` -> `config_workflow_wires`
|
|
299
|
+
* rows, old feedback id -> v2 wire id. Wire ids reflect Amendment f as revised
|
|
300
|
+
* (single-wire inputs; the double-loop seeds' blocking wires keep their ids as
|
|
301
|
+
* the OR valve's in-wires — exactly where budgets and gates now count, so
|
|
302
|
+
* migrated overlays land correctly by construction).
|
|
303
|
+
*
|
|
304
|
+
* wf_no-clarify's fb_0 is the refine self-wire and fb_1 the review loop wire —
|
|
305
|
+
* the user's max_cycles=6 overlays ride those two. wf_clarify-implement's fb
|
|
306
|
+
* order is BELIEVED swapped in the DB and is UNVERIFIED (that template is absent
|
|
307
|
+
* from the reference DB); V17's dynamic resolver, not this row, is what makes
|
|
308
|
+
* the migration safe. */
|
|
309
|
+
export const FB_WIRE_MAP = deepFreeze({
|
|
310
|
+
wf_full: { fb_0: 'w5', fb_1: 'w12', fb_2: 'w15' },
|
|
311
|
+
'wf_no-clarify': { fb_0: 'w3', fb_1: 'w10' },
|
|
312
|
+
'wf_provided-plan': { fb_0: 'w2', fb_1: 'w9', fb_2: 'w12' },
|
|
313
|
+
'wf_full-no-decompose': { fb_0: 'w5', fb_1: 'w10', fb_2: 'w13' },
|
|
314
|
+
'wf_quick-fix': { fb_0: 'w5' },
|
|
315
|
+
'wf_clarify-implement': { fb_0: 'w9', fb_1: 'w5' },
|
|
316
|
+
'wf_clarify-quick-fix': { fb_0: 'w7' },
|
|
317
|
+
wf_default: { fb_refine: 'w5', fb_review: 'w9' },
|
|
318
|
+
});
|
package/src/core/model-env.mjs
CHANGED
|
@@ -20,13 +20,17 @@ export const EFFORTS = ['medium', 'high', 'xhigh', 'max'];
|
|
|
20
20
|
// Env keys a model entry may NOT set (§4.4): process fundamentals and worca's
|
|
21
21
|
// own runtime knobs, any of which injection could otherwise subvert (mock
|
|
22
22
|
// mode, the claude binary path, the effort flag name). Everything else —
|
|
23
|
-
// including all ANTHROPIC_* / CLAUDE_* — is allowed: routing them is the
|
|
23
|
+
// including all other ANTHROPIC_* / CLAUDE_* — is allowed: routing them is the
|
|
24
24
|
// point. CLAUDE_CODE_SUBPROCESS_ENV_SCRUB is the CLI-2.1.220 permission-mode
|
|
25
25
|
// landmine documented at claude-runner.mjs#buildSpawnEnv.
|
|
26
|
+
// CLAUDE_CODE_SUBAGENT_MODEL is reserved too: the per-node `subagentModel`
|
|
27
|
+
// prompt policy is the only sanctioned wire for a child's model, and a catalog
|
|
28
|
+
// entry silently flooring every fan-out child would contradict the per-node
|
|
29
|
+
// control the UI shows.
|
|
26
30
|
export const RESERVED_MODEL_ENV_KEYS = [
|
|
27
31
|
'PATH', 'HOME', 'TMPDIR', 'SHELL', 'USER', 'LOGNAME', 'TERM',
|
|
28
32
|
'NODE_OPTIONS', 'NODE_EXTRA_CA_CERTS',
|
|
29
|
-
'CLAUDECODE', 'CLAUDE_CODE_SUBPROCESS_ENV_SCRUB',
|
|
33
|
+
'CLAUDECODE', 'CLAUDE_CODE_SUBPROCESS_ENV_SCRUB', 'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
30
34
|
];
|
|
31
35
|
export const RESERVED_MODEL_ENV_PREFIXES = ['WORCA_'];
|
|
32
36
|
|
|
@@ -49,9 +53,12 @@ export function modelEnvRef(value) {
|
|
|
49
53
|
/**
|
|
50
54
|
* Defensive spawn-time pass over a model entry's env (§4.4): drops reserved
|
|
51
55
|
* keys and non-string values, and expands whole-value ${VAR} refs from
|
|
52
|
-
* `sourceEnv` (an unset or empty var drops the key).
|
|
53
|
-
*
|
|
54
|
-
*
|
|
56
|
+
* `sourceEnv` (an unset or empty var drops the key). Values are trimmed and a
|
|
57
|
+
* value that is empty after trimming drops the key — an ANTHROPIC_MODEL wire id
|
|
58
|
+
* (#374) reaches `--model` verbatim, so a stray-whitespace or empty value must
|
|
59
|
+
* not survive. The user catalog rejects reserved keys at write time and plugin
|
|
60
|
+
* manifests strip them at normalize time, so a drop here means a hand-edited
|
|
61
|
+
* settings file — the caller owns warning about `dropped`.
|
|
55
62
|
* @param {Record<string,*>|undefined} modelEnv
|
|
56
63
|
* @param {Record<string,string|undefined>} [sourceEnv]
|
|
57
64
|
* @returns {{env: Record<string,string>, dropped: string[]}}
|
|
@@ -64,11 +71,108 @@ export function prepareModelEnv(modelEnv, sourceEnv = process.env) {
|
|
|
64
71
|
const ref = modelEnvRef(v);
|
|
65
72
|
if (ref !== null) {
|
|
66
73
|
const resolved = sourceEnv ? sourceEnv[ref] : undefined;
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
const t = typeof resolved === 'string' ? resolved.trim() : '';
|
|
75
|
+
if (!t) { dropped.push(k); continue; }
|
|
76
|
+
env[k] = t;
|
|
69
77
|
} else {
|
|
70
|
-
|
|
78
|
+
const t = v.trim();
|
|
79
|
+
if (!t) { dropped.push(k); continue; }
|
|
80
|
+
env[k] = t;
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
return { env, dropped };
|
|
74
84
|
}
|
|
85
|
+
|
|
86
|
+
// ── per-model cost override (opt-in pricing, config.mjs resolveModelCost) ─────
|
|
87
|
+
// Lives HERE for the same reason the env policy does: BOTH catalog layers must
|
|
88
|
+
// validate it against one rule. settings.mjs owns the user's global catalog and
|
|
89
|
+
// plugin-manifest.mjs owns a plugin's models — neither may import the other, and
|
|
90
|
+
// this leaf imports nothing. A plugin that ships a model routed at its own
|
|
91
|
+
// endpoint is exactly the case that needs a price pinned, so its manifest
|
|
92
|
+
// carries `cost` with the same shape and the same validation as a global entry.
|
|
93
|
+
|
|
94
|
+
/** Allowed per-million-token rate keys for a model's `cost.perMtok` table. */
|
|
95
|
+
export const COST_RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite', 'cacheWrite1h'];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate a model `cost` override. Returns the normalized shape or undefined;
|
|
99
|
+
* THROWS on malformed input (callers that must not throw catch and drop).
|
|
100
|
+
* { free: true } → recorded spend is always $0
|
|
101
|
+
* { perMtok: { input, output, cacheRead, … } } → USD per million tokens, >= 0
|
|
102
|
+
* `{ free: false }` / `{}` mean "no override" → undefined.
|
|
103
|
+
* @param {*} cost
|
|
104
|
+
* @returns {{free:true}|{perMtok:Record<string,number>}|undefined}
|
|
105
|
+
* @throws {Error}
|
|
106
|
+
*/
|
|
107
|
+
export function assertModelCost(cost) {
|
|
108
|
+
if (cost === undefined || cost === null) return undefined;
|
|
109
|
+
if (typeof cost !== 'object' || Array.isArray(cost)) throw new Error('cost must be an object');
|
|
110
|
+
if (cost.free !== undefined && typeof cost.free !== 'boolean') throw new Error('cost.free must be a boolean');
|
|
111
|
+
if (cost.free === true) return { free: true };
|
|
112
|
+
if (cost.perMtok !== undefined) {
|
|
113
|
+
const p = cost.perMtok;
|
|
114
|
+
if (!p || typeof p !== 'object' || Array.isArray(p)) {
|
|
115
|
+
throw new Error('cost.perMtok must be an object of USD-per-million-token rates');
|
|
116
|
+
}
|
|
117
|
+
const rates = {};
|
|
118
|
+
for (const [k, v] of Object.entries(p)) {
|
|
119
|
+
if (!COST_RATE_KEYS.includes(k)) {
|
|
120
|
+
throw new Error(`unknown cost.perMtok rate ${JSON.stringify(k)} — allowed: ${COST_RATE_KEYS.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
const n = Number(v);
|
|
123
|
+
if (!Number.isFinite(n) || n < 0) throw new Error(`cost.perMtok.${k} must be a finite number >= 0`);
|
|
124
|
+
rates[k] = n;
|
|
125
|
+
}
|
|
126
|
+
if (!Object.keys(rates).length) throw new Error('cost.perMtok must define at least one rate');
|
|
127
|
+
return { perMtok: rates };
|
|
128
|
+
}
|
|
129
|
+
return undefined; // { free: false } or {} — no override
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── sub-agent model policy (per-node `subagentModel`) ─────────────────────────
|
|
133
|
+
// What a fan-out node's Task/Agent children run on. ONE wire — a prompt block
|
|
134
|
+
// (phases.mjs#subagentModelDirective) that tells the agent to pass `model` on
|
|
135
|
+
// every Task call — because the CLI resolves a child's model as Task-call
|
|
136
|
+
// `model` > the agent definition's own `model:` frontmatter > env default >
|
|
137
|
+
// parent: only the explicit Task-level value reliably binds every child. (The
|
|
138
|
+
// earlier CLAUDE_CODE_SUBAGENT_MODEL env floor was removed for exactly that
|
|
139
|
+
// reason — it bound only agents with no model key — and the key is reserved
|
|
140
|
+
// above so a catalog entry cannot resurrect it.)
|
|
141
|
+
//
|
|
142
|
+
// The vocabulary is deliberately NOT the worca catalog: the CLI's Task tool
|
|
143
|
+
// accepts an ALIAS enum, so a catalog id (or an ANTHROPIC_MODEL wire id) would
|
|
144
|
+
// be rejected at spawn time. Haiku is excluded by product decision.
|
|
145
|
+
export const SUBAGENT_MODELS = ['sonnet', 'opus', 'fable'];
|
|
146
|
+
|
|
147
|
+
/** "the agent picks per Task call" — a choice rubric in the prompt. */
|
|
148
|
+
export const SUBAGENT_AUTO = 'auto';
|
|
149
|
+
|
|
150
|
+
/** "children ride the CLI's own resolution" (an agent definition's frontmatter,
|
|
151
|
+
* else the parent's model) — the pre-feature prompt. Stored explicitly,
|
|
152
|
+
* because the DEFAULT for an unset node is `auto`, not this. */
|
|
153
|
+
export const SUBAGENT_INHERIT = 'inherit';
|
|
154
|
+
|
|
155
|
+
/** Every storable `subagentModel`. '' / absent are NOT storable — they mean
|
|
156
|
+
* "unset", which the runtime resolves to SUBAGENT_DEFAULT. */
|
|
157
|
+
export const SUBAGENT_MODEL_VALUES = [...SUBAGENT_MODELS, SUBAGENT_AUTO, SUBAGENT_INHERIT];
|
|
158
|
+
|
|
159
|
+
/** What an unset node resolves to at run time: agents choose BY DEFAULT. */
|
|
160
|
+
export const SUBAGENT_DEFAULT = SUBAGENT_AUTO;
|
|
161
|
+
|
|
162
|
+
/** Whether `v` is a storable subagentModel. */
|
|
163
|
+
export function isSubagentModelValue(v) {
|
|
164
|
+
return typeof v === 'string' && SUBAGENT_MODEL_VALUES.includes(v);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The policy a raw stored value puts in force: a legal value is itself; '' /
|
|
168
|
+
* absent / anything that escaped validation is the auto default. */
|
|
169
|
+
export function effectiveSubagentModel(v) {
|
|
170
|
+
return isSubagentModelValue(v) ? v : SUBAGENT_DEFAULT;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The one validation message every writer shares, '' when `v` is acceptable
|
|
174
|
+
* ('' and null/undefined mean clear/absent and are always fine). */
|
|
175
|
+
export function subagentModelIssue(v) {
|
|
176
|
+
if (v == null || v === '' || isSubagentModelValue(v)) return '';
|
|
177
|
+
return `unknown sub-agent model ${JSON.stringify(String(v))}`;
|
|
178
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/core/model-test.mjs
|
|
2
|
+
// One minimal live claude spawn to verify a catalog model actually works —
|
|
3
|
+
// the Models-view Test button (POST /api/models/:id/test). Mirrors the cheap
|
|
4
|
+
// aux-run pattern of title.mjs: the model's catalog routing env travels with
|
|
5
|
+
// the id via resolveModelEnv, no tools, low effort, hard timeout. Never
|
|
6
|
+
// throws — the outcome is a result object either way.
|
|
7
|
+
|
|
8
|
+
import { runClaude } from './claude-runner.mjs';
|
|
9
|
+
import { resolveModelEnv } from './config.mjs';
|
|
10
|
+
import { classifyError } from './recoverable-error.mjs';
|
|
11
|
+
|
|
12
|
+
const TEST_TIMEOUT_MS = 60_000;
|
|
13
|
+
const REPLY_CAP = 100;
|
|
14
|
+
|
|
15
|
+
const SYSTEM = 'You are a connectivity check. Reply with exactly OK.';
|
|
16
|
+
|
|
17
|
+
/** Actionable hint for a recovery class ('' when there is nothing to add —
|
|
18
|
+
* the raw runner message already carries the detail). Pure, for tests. */
|
|
19
|
+
export function hintFor(errorClass) {
|
|
20
|
+
switch (errorClass) {
|
|
21
|
+
case 'auth': return 'authentication failed — check the token/secret for this model';
|
|
22
|
+
case 'network': return 'endpoint unreachable — check ANTHROPIC_BASE_URL';
|
|
23
|
+
case 'rate_limit': return 'the endpoint is rate-limiting or overloaded — try again shortly';
|
|
24
|
+
case 'quota': return 'quota/billing problem — check the account behind this endpoint';
|
|
25
|
+
case 'usage_limit': return 'usage limit reached on the account behind this endpoint';
|
|
26
|
+
case 'timeout': return `no reply — the test timed out after ${TEST_TIMEOUT_MS / 1000}s`;
|
|
27
|
+
default: return '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Live connectivity check for a catalog model id (global or plugin — the
|
|
33
|
+
* resolution precedence is resolveModelEnv's). Explicit user action only.
|
|
34
|
+
* @param {string} id catalog model id
|
|
35
|
+
* @param {{signal?:AbortSignal, bin?:string, run?:typeof runClaude}} [opts]
|
|
36
|
+
* `run` is injectable for unit tests.
|
|
37
|
+
* @returns {Promise<{ok:true, text:string}|{ok:false, errorClass:(string|null), message:string, hint?:string}>}
|
|
38
|
+
*/
|
|
39
|
+
export async function testModel(id, { signal, bin, run = runClaude } = {}) {
|
|
40
|
+
const ctrl = new AbortController();
|
|
41
|
+
const onOuterAbort = () => ctrl.abort();
|
|
42
|
+
if (signal) {
|
|
43
|
+
if (signal.aborted) ctrl.abort();
|
|
44
|
+
else signal.addEventListener('abort', onOuterAbort, { once: true });
|
|
45
|
+
}
|
|
46
|
+
const timer = setTimeout(() => ctrl.abort(), TEST_TIMEOUT_MS);
|
|
47
|
+
timer.unref?.();
|
|
48
|
+
try {
|
|
49
|
+
const { text } = await run({
|
|
50
|
+
cwd: process.cwd(),
|
|
51
|
+
systemPrompt: SYSTEM,
|
|
52
|
+
prompt: 'Reply with exactly OK.',
|
|
53
|
+
model: id,
|
|
54
|
+
modelEnv: resolveModelEnv(id),
|
|
55
|
+
effort: 'low',
|
|
56
|
+
permissionMode: 'acceptEdits',
|
|
57
|
+
allowedTools: [], // empty → no --allowedTools flag; pure text gen
|
|
58
|
+
signal: ctrl.signal,
|
|
59
|
+
bin,
|
|
60
|
+
onEvent: () => {},
|
|
61
|
+
});
|
|
62
|
+
const reply = String(text || '').split(/\r?\n/).map((l) => l.trim()).find((l) => l) || '';
|
|
63
|
+
if (!reply) {
|
|
64
|
+
return { ok: false, errorClass: null, message: 'the model returned an empty reply' };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, text: reply.slice(0, REPLY_CAP) };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err && err.name === 'AbortError') {
|
|
69
|
+
return { ok: false, errorClass: 'timeout', message: `Timed out after ${TEST_TIMEOUT_MS / 1000}s`, hint: hintFor('timeout') };
|
|
70
|
+
}
|
|
71
|
+
const message = err && err.message ? err.message : String(err);
|
|
72
|
+
const errorClass = (err && err.errorClass) || classifyError(message);
|
|
73
|
+
const hint = hintFor(errorClass);
|
|
74
|
+
return { ok: false, errorClass, message, ...(hint ? { hint } : {}) };
|
|
75
|
+
} finally {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
if (signal) signal.removeEventListener?.('abort', onOuterAbort);
|
|
78
|
+
}
|
|
79
|
+
}
|