@worca/app 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +85 -6
  2. package/agents/clarify.meta.json +1 -0
  3. package/agents/memoryDefragmenter.meta.json +2 -1
  4. package/agents/reviewer.meta.json +60 -0
  5. package/agents/worca-cc-code-reviewer.md +33 -0
  6. package/agents/worca-cc-memory-defragmenter.md +5 -3
  7. package/agents/workspaceScanner.meta.json +1 -0
  8. package/package.json +14 -10
  9. package/scripts/git-diff.mjs +25 -0
  10. package/scripts/gitDiff.meta.json +18 -0
  11. package/scripts/js-inline.mjs +11 -0
  12. package/scripts/js.meta.json +22 -0
  13. package/scripts/py-inline.py +27 -0
  14. package/scripts/py.meta.json +22 -0
  15. package/scripts/shell.meta.json +24 -0
  16. package/skills/worca/SKILL.md +3 -2
  17. package/src/cli/models.mjs +247 -0
  18. package/src/cli/render.mjs +72 -4
  19. package/src/cli/schedule.mjs +494 -0
  20. package/src/cli/worca-cc.mjs +1001 -22
  21. package/src/core/agent-registry.mjs +75 -23
  22. package/src/core/agent-store.mjs +51 -2
  23. package/src/core/artifacts.mjs +73 -10
  24. package/src/core/ask/events.mjs +119 -1
  25. package/src/core/ask/limits.mjs +32 -4
  26. package/src/core/ask/mcp-stdio.mjs +12 -0
  27. package/src/core/ask/model-deps.mjs +126 -0
  28. package/src/core/ask/model-proposal.mjs +370 -0
  29. package/src/core/ask/models.mjs +12 -0
  30. package/src/core/ask/policy-deps.mjs +124 -0
  31. package/src/core/ask/policy-proposal.mjs +363 -0
  32. package/src/core/ask/prompt.mjs +74 -9
  33. package/src/core/ask/proposal.mjs +54 -5
  34. package/src/core/ask/schedule-deps.mjs +83 -0
  35. package/src/core/ask/schedule-spec.mjs +310 -0
  36. package/src/core/ask/script-deps.mjs +357 -0
  37. package/src/core/ask/source-deps.mjs +52 -0
  38. package/src/core/ask/source-spec.mjs +157 -0
  39. package/src/core/ask/spawn.mjs +1 -0
  40. package/src/core/ask/store.mjs +6 -3
  41. package/src/core/ask/tool-deps.mjs +4 -0
  42. package/src/core/ask/tools.mjs +657 -37
  43. package/src/core/ask/turn.mjs +109 -2
  44. package/src/core/ask-files.mjs +406 -0
  45. package/src/core/ask-forms.mjs +195 -0
  46. package/src/core/ask-projection.mjs +72 -0
  47. package/src/core/bridge/errors.mjs +84 -0
  48. package/src/core/bridge/provider-ops.mjs +281 -0
  49. package/src/core/bridge/providers/copilot.mjs +269 -0
  50. package/src/core/bridge/providers/endpoint.mjs +257 -0
  51. package/src/core/bridge/registry.mjs +88 -0
  52. package/src/core/bridge/semaphore.mjs +73 -0
  53. package/src/core/bridge/server.mjs +184 -0
  54. package/src/core/bridge/telemetry.mjs +53 -0
  55. package/src/core/bridge/translate/request.mjs +252 -0
  56. package/src/core/bridge/translate/response.mjs +82 -0
  57. package/src/core/bridge/translate/stream.mjs +242 -0
  58. package/src/core/bridge/upstream.mjs +209 -0
  59. package/src/core/chat/command-router.mjs +58 -4
  60. package/src/core/chat/notifier.mjs +14 -1
  61. package/src/core/chat/renderers.mjs +35 -0
  62. package/src/core/claude-runner.mjs +126 -19
  63. package/src/core/config.mjs +212 -31
  64. package/src/core/cost-budget.mjs +3 -2
  65. package/src/core/db.mjs +169 -15
  66. package/src/core/failure-policy.mjs +10 -0
  67. package/src/core/fs-browse.mjs +16 -4
  68. package/src/core/git-info.mjs +22 -0
  69. package/src/core/graph/builtin-workflows.mjs +3 -1
  70. package/src/core/graph/exec-io.mjs +71 -0
  71. package/src/core/graph/executor.mjs +139 -70
  72. package/src/core/graph/human-evidence.mjs +131 -0
  73. package/src/core/graph/python-probe.mjs +172 -0
  74. package/src/core/graph/registry-ports.mjs +10 -6
  75. package/src/core/graph/scheduler.mjs +39 -24
  76. package/src/core/graph/script-child.mjs +81 -0
  77. package/src/core/graph/script-runner.mjs +597 -0
  78. package/src/core/graph/worca_script.py +207 -0
  79. package/src/core/guardrail-store.mjs +16 -0
  80. package/src/core/human-backfill.mjs +108 -0
  81. package/src/core/human-rate.mjs +17 -0
  82. package/src/core/index-html.mjs +6 -2
  83. package/src/core/memory-defrag-model.mjs +112 -0
  84. package/src/core/memory-store.mjs +70 -18
  85. package/src/core/memory-sync.mjs +22 -11
  86. package/src/core/metrics/read.mjs +4 -1
  87. package/src/core/metrics/record.mjs +50 -2
  88. package/src/core/metrics/sync.mjs +6 -4
  89. package/src/core/model-env.mjs +149 -0
  90. package/src/core/model-test.mjs +14 -1
  91. package/src/core/notifications.mjs +128 -0
  92. package/src/core/onboarding.mjs +8 -2
  93. package/src/core/orchestrator.mjs +357 -26
  94. package/src/core/phases.mjs +95 -6
  95. package/src/core/plugin-api.mjs +24 -7
  96. package/src/core/plugin-manifest.mjs +184 -18
  97. package/src/core/plugin-models.mjs +1 -0
  98. package/src/core/plugin-script-cases.mjs +118 -0
  99. package/src/core/plugin-store.mjs +163 -17
  100. package/src/core/plugin-workflows.mjs +71 -17
  101. package/src/core/policy/cache.mjs +116 -0
  102. package/src/core/policy/effective.mjs +175 -0
  103. package/src/core/policy/gate.mjs +91 -0
  104. package/src/core/policy/local.mjs +145 -0
  105. package/src/core/policy/registry.mjs +330 -0
  106. package/src/core/policy/scope.mjs +61 -0
  107. package/src/core/policy/state.mjs +79 -0
  108. package/src/core/policy/sync.mjs +513 -0
  109. package/src/core/protocol.mjs +43 -0
  110. package/src/core/run-harness.mjs +421 -72
  111. package/src/core/scheduler.mjs +980 -0
  112. package/src/core/script-bench.mjs +628 -0
  113. package/src/core/script-registry.mjs +116 -0
  114. package/src/core/script-store.mjs +563 -0
  115. package/src/core/settings.mjs +489 -14
  116. package/src/core/stats.mjs +33 -2
  117. package/src/core/workflow-export.mjs +94 -3
  118. package/src/core/workflow-share.mjs +67 -18
  119. package/src/core/workflows.mjs +47 -11
  120. package/src/core/workspaces.mjs +18 -12
  121. package/src/shared/forms/answer.mjs +164 -0
  122. package/src/shared/forms/catalog.mjs +91 -0
  123. package/src/shared/forms/form-def.mjs +290 -0
  124. package/src/shared/forms/layout.mjs +67 -0
  125. package/src/shared/forms/paths.mjs +47 -0
  126. package/src/shared/forms/project.mjs +309 -0
  127. package/src/shared/forms/schema.mjs +205 -0
  128. package/src/shared/graph/agent-meta.mjs +55 -5
  129. package/src/shared/graph/constants.mjs +14 -2
  130. package/src/shared/graph/flow-layout.mjs +2 -1
  131. package/src/shared/graph/isomorphic.mjs +5 -3
  132. package/src/shared/graph/manifest.mjs +22 -13
  133. package/src/shared/graph/ports.mjs +45 -19
  134. package/src/shared/graph/script-cases.mjs +257 -0
  135. package/src/shared/graph/script-icons.mjs +46 -0
  136. package/src/shared/graph/script-infer.mjs +259 -0
  137. package/src/shared/graph/script-meta.mjs +408 -0
  138. package/src/shared/graph/script-templates.mjs +201 -0
  139. package/src/shared/graph/template.mjs +4 -4
  140. package/src/shared/graph/validate.mjs +89 -16
  141. package/src/shared/human-estimate.mjs +100 -0
  142. package/src/shared/schedule/recurrence.mjs +353 -0
  143. package/src/shared/team-metrics/aggregate.mjs +51 -11
  144. package/{scripts → tools}/install.mjs +3 -3
  145. package/ui/public/app.js +4390 -683
  146. package/ui/public/artifact-picker.mjs +189 -0
  147. package/ui/public/ask/dom.mjs +121 -0
  148. package/ui/public/ask/form-preview.mjs +55 -0
  149. package/ui/public/ask/form-renderer.mjs +250 -0
  150. package/ui/public/ask/registry.mjs +53 -0
  151. package/ui/public/ask/widgets-display.mjs +370 -0
  152. package/ui/public/ask/widgets-input.mjs +624 -0
  153. package/ui/public/ask/widgets-layout.mjs +90 -0
  154. package/ui/public/ask-panel.mjs +401 -27
  155. package/ui/public/ask-run-card.mjs +1 -1
  156. package/ui/public/bridge-view.mjs +694 -0
  157. package/ui/public/chat-settings-view.mjs +24 -0
  158. package/ui/public/code-editor.mjs +181 -0
  159. package/ui/public/getting-started.mjs +34 -7
  160. package/ui/public/graph/composer.mjs +138 -10
  161. package/ui/public/graph/inspector.mjs +61 -58
  162. package/ui/public/graph/palette.mjs +27 -9
  163. package/ui/public/graph/run-decor.mjs +34 -17
  164. package/ui/public/graph/run-hosts.mjs +7 -1
  165. package/ui/public/graph/save-dialog.mjs +3 -0
  166. package/ui/public/graph/view.mjs +23 -7
  167. package/ui/public/guardrails-view.mjs +15 -3
  168. package/ui/public/guide-spot.mjs +87 -9
  169. package/ui/public/index.html +480 -141
  170. package/ui/public/memory-view.mjs +22 -4
  171. package/ui/public/models-view.mjs +162 -17
  172. package/ui/public/node-tunables.mjs +33 -4
  173. package/ui/public/plugins-view.mjs +23 -1
  174. package/ui/public/results-view.mjs +4 -2
  175. package/ui/public/schedule-sheet.mjs +430 -0
  176. package/ui/public/schedules-view.mjs +432 -0
  177. package/ui/public/script-bench-view.mjs +1154 -0
  178. package/ui/public/script-forms.mjs +282 -0
  179. package/ui/public/script-wizard.mjs +529 -0
  180. package/ui/public/scripts-view.mjs +868 -0
  181. package/ui/public/stats-view.mjs +159 -52
  182. package/ui/public/style.css +1461 -44
  183. package/ui/public/team-metrics-surfaces.mjs +77 -16
  184. package/ui/public/team-metrics-view.mjs +68 -5
  185. package/ui/public/team-policy-view.mjs +1402 -0
  186. package/ui/public/ui-level.mjs +237 -0
  187. package/ui/server.mjs +1827 -73
@@ -80,6 +80,7 @@ export function weekStartMs(ms) {
80
80
 
81
81
  const startOf = (r) => Date.parse(r.startedAt);
82
82
  const usdOf = (r) => (Number.isFinite(r?.cost?.usd) ? r.cost.usd : 0);
83
+ const hoursOf = (r) => (Number.isFinite(r?.human?.hours) ? r.human.hours : 0);
83
84
  const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
84
85
  const sum = (a) => a.reduce((s, x) => s + x, 0);
85
86
  const round2 = (v) => Math.round(v * 100) / 100;
@@ -127,18 +128,26 @@ export function applyFilter(records, filter = {}) {
127
128
  return records.filter((r) => active.every(([dim, key]) => DIMENSIONS[dim](r).some((k) => k.key === key)));
128
129
  }
129
130
 
130
- function computeKpis(rs, now) {
131
+ function computeKpis(rs, now, humanRateUsd = 0) {
131
132
  const n = rs.length;
132
133
  const count = (res) => rs.filter((r) => r.result === res).length;
133
134
  const usd = sum(rs.map(usdOf));
135
+ const hours = round2(sum(rs.map(hoursOf)));
134
136
  const paired = rs.filter((r) => isNum(r.wallMs) && isNum(r.activeMs));
135
- const wallSum = sum(paired.map((r) => r.wallMs));
137
+ // Autonomy = active ÷ (wall − paused): a run parked on a pause or a crash was not waiting on a
138
+ // human, so its parked time leaves the denominator. `pausedMs` is additive — records pushed
139
+ // before it existed park nothing.
140
+ const wallSum = sum(paired.map((r) => Math.max(0, r.wallMs - (isNum(r.pausedMs) ? r.pausedMs : 0))));
136
141
  const withPr = rs.filter((r) => r.pr && (r.pr.url || r.pr.number != null));
137
142
  const reviews = rs.map((r) => r.cycles?.review).filter(isNum);
138
143
  const d = new Date(now);
139
144
  const monthStart = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
140
145
  return {
141
146
  spendUsd: round2(usd),
147
+ // Human hours (money-saved design §9.2): records without `human` count 0 h; `humanRuns` gates the tile.
148
+ humanHours: hours,
149
+ humanRuns: rs.filter((r) => Number.isFinite(r?.human?.hours)).length,
150
+ savedUsd: round2(hours * humanRateUsd - usd),
142
151
  spendThisMonthUsd: round2(sum(rs.filter((r) => startOf(r) >= monthStart).map(usdOf))),
143
152
  runs: n,
144
153
  done: count('done'), failed: count('failed'), stopped: count('stopped'),
@@ -155,14 +164,26 @@ function computeKpis(rs, now) {
155
164
  reviewCyclesMean: reviews.length ? round1(sum(reviews) / reviews.length) : null,
156
165
  convergeInOneRate: reviews.length ? reviews.filter((c) => c <= 1).length / reviews.length : null,
157
166
  filesChanged: sum(rs.map((r) => r.git?.filesChanged).filter(isNum)),
167
+ // Team policy (team-policy design §10): runs that went past a team cap (continued past, or
168
+ // exceeded under warn / unattended) or picked something off-policy. Additive; policy-less
169
+ // records count nothing.
170
+ offPolicyRuns: rs.filter((r) => policyOverrides(r) + policyDeviations(r) > 0).length,
171
+ capOverrides: sum(rs.map(policyOverrides)),
172
+ deviations: sum(rs.map(policyDeviations)),
173
+ policyRuns: rs.filter((r) => r.policy && r.policy.home).length,
158
174
  };
159
175
  }
160
176
 
177
+ const policyOverrides = (r) => (r.policy && r.policy.home ? (Array.isArray(r.policy.overrides) ? r.policy.overrides.length : 0) + (Array.isArray(r.policy.exceeded) ? r.policy.exceeded.length : 0) : 0);
178
+ const policyDeviations = (r) => (r.policy && r.policy.home && Array.isArray(r.policy.deviations) ? r.policy.deviations.length : 0);
179
+
161
180
  const pct = (cur, prev) => (isNum(cur) && isNum(prev) && prev > 0 ? (cur - prev) / prev : null);
162
181
 
163
182
  function deltas(k, p) {
164
183
  return {
165
184
  spendPct: pct(k.spendUsd, p.spendUsd),
185
+ humanHoursPct: pct(k.humanHours, p.humanHours),
186
+ savedPct: pct(k.savedUsd, p.savedUsd),
166
187
  runsPct: pct(k.runs, p.runs),
167
188
  costPerRunPct: pct(k.costPerRunUsd, p.costPerRunUsd),
168
189
  durationPct: pct(k.durationMedianMs, p.durationMedianMs),
@@ -175,9 +196,11 @@ function breakdown(rs, dim) {
175
196
  const rows = new Map();
176
197
  for (const r of rs) {
177
198
  for (const k of DIMENSIONS[dim](r)) {
178
- const row = rows.get(k.key) || { key: k.key, label: k.label, sub: k.sub ?? null, runs: 0, usd: 0, done: 0, reviews: [], filesChanged: 0, filesUnknown: 0 };
199
+ const row = rows.get(k.key) || { key: k.key, label: k.label, sub: k.sub ?? null, runs: 0, usd: 0, done: 0, reviews: [], filesChanged: 0, filesUnknown: 0, overrides: 0, offPolicy: 0 };
179
200
  row.runs += 1;
180
201
  row.usd += usdOf(r); // "a run touching two projects counts in both"
202
+ row.overrides += policyOverrides(r); // team caps continued past / exceeded (team policy §10)
203
+ row.offPolicy += policyDeviations(r);
181
204
  if (r.result === 'done') row.done += 1;
182
205
  if (isNum(r.cycles?.review)) row.reviews.push(r.cycles.review);
183
206
  // A key that carries its own count (a touched member) uses it; every other dimension is
@@ -207,6 +230,8 @@ function breakdown(rs, dim) {
207
230
  // null when no run in the row carries a count: the cell shows "–", never a wrong number.
208
231
  filesChanged: row.filesUnknown === row.runs ? null : row.filesChanged,
209
232
  filesUnknown: row.filesUnknown,
233
+ overrides: row.overrides,
234
+ offPolicy: row.offPolicy,
210
235
  isNone: row.key === NONE,
211
236
  }))
212
237
  // Projects order by what the table shows (runs touched, then files); every other dimension by spend.
@@ -271,11 +296,17 @@ export function safeHttpUrl(u) {
271
296
  try { const x = new URL(u); return x.protocol === 'https:' || x.protocol === 'http:' ? x.href : null; } catch { return null; }
272
297
  }
273
298
 
274
- function toRunRow(r) {
299
+ function toRunRow(r, humanRateUsd = 0) {
300
+ const usd = usdOf(r);
301
+ // Money saved (design §9.2) per run: null when the record carries no `human` (pre-v33 pushes),
302
+ // so the row prints "—" rather than claiming "−$cost" was saved. Same gate as the KPI tile.
303
+ const humanHours = isNum(r.human?.hours) ? r.human.hours : null;
275
304
  return {
276
305
  id: r.id, title: r.title ?? '(untitled)', startedAt: r.startedAt,
277
306
  workflow: r.workflow?.name ?? r.workflow?.id ?? null, result: r.result,
278
- usd: usdOf(r), wallMs: isNum(r.wallMs) ? r.wallMs : null, activeMs: isNum(r.activeMs) ? r.activeMs : null,
307
+ usd, wallMs: isNum(r.wallMs) ? r.wallMs : null, activeMs: isNum(r.activeMs) ? r.activeMs : null,
308
+ pausedMs: isNum(r.pausedMs) ? r.pausedMs : null,
309
+ humanHours, savedUsd: humanHours == null ? null : round2(humanHours * humanRateUsd - usd),
279
310
  reviewCycles: isNum(r.cycles?.review) ? r.cycles.review : null,
280
311
  pr: r.pr && (r.pr.url || r.pr.number != null) ? { number: Number.isInteger(r.pr.number) ? r.pr.number : null, url: safeHttpUrl(r.pr.url) } : null,
281
312
  actor: r.actor ?? null,
@@ -283,6 +314,15 @@ function toRunRow(r) {
283
314
  projects: r.target?.kind === 'workspace'
284
315
  ? (Array.isArray(r.target.touched) ? r.target.touched.filter((p) => typeof p === 'string') : [])
285
316
  : [r.target?.project].filter((p) => typeof p === 'string' && p),
317
+ // Team policy (§10): only on a run recorded under a policy, so policy-less rows keep their shape.
318
+ ...(r.policy && r.policy.home ? { policy: {
319
+ home: r.policy.home,
320
+ overrides: Array.isArray(r.policy.overrides) ? r.policy.overrides : [],
321
+ exceeded: Array.isArray(r.policy.exceeded) ? r.policy.exceeded : [],
322
+ deviations: Array.isArray(r.policy.deviations) ? r.policy.deviations : [],
323
+ unattended: r.policy.unattended === true,
324
+ reason: typeof r.policy.reason === 'string' ? r.policy.reason : null,
325
+ } } : {}),
286
326
  };
287
327
  }
288
328
 
@@ -290,15 +330,15 @@ function toRunRow(r) {
290
330
  * @param {object[]} records v1 records (already parsed; unknown v / malformed already dropped)
291
331
  * @param {{range?:string, from?:string|null, to?:string|null, groupBy?:string, filter?:object, now?:number}} [opts]
292
332
  */
293
- export function aggregate(records, { range = 'this-month', from = null, to = null, groupBy = 'workflow', filter = {}, now = Date.now() } = {}) {
333
+ export function aggregate(records, { range = 'this-month', from = null, to = null, groupBy = 'workflow', filter = {}, now = Date.now(), humanRateUsd = 0 } = {}) {
294
334
  if (!GROUP_BYS.includes(groupBy)) throw new RangeError(`unknown groupBy "${groupBy}" (expected ${GROUP_BYS.join(' | ')})`);
295
335
  const win = resolveRange(range, { now, from, to });
296
336
  const usable = (records || []).filter((r) => r && Number.isFinite(startOf(r)));
297
337
  const filtered = applyFilter(usable, filter);
298
338
  const inWin = (s, e) => (r) => (s == null || startOf(r) >= s) && (e == null || startOf(r) < e);
299
339
  const inRange = filtered.filter(inWin(win.startMs, win.endMs));
300
- const kpis = computeKpis(inRange, now);
301
- const prevKpis = win.prevStartMs == null ? null : computeKpis(filtered.filter(inWin(win.prevStartMs, win.prevEndMs)), now);
340
+ const kpis = computeKpis(inRange, now, humanRateUsd);
341
+ const prevKpis = win.prevStartMs == null ? null : computeKpis(filtered.filter(inWin(win.prevStartMs, win.prevEndMs)), now, humanRateUsd);
302
342
  const hasActor = inRange.some((r) => r.actor);
303
343
  const hasWorkspace = inRange.some((r) => r.target?.kind === 'workspace');
304
344
  return {
@@ -312,12 +352,12 @@ export function aggregate(records, { range = 'this-month', from = null, to = nul
312
352
  project: hasWorkspace ? breakdown(inRange, 'project') : null,
313
353
  models: breakdown(inRange, 'models'),
314
354
  },
315
- runs: [...inRange].sort((a, b) => startOf(b) - startOf(a)).map(toRunRow),
355
+ runs: [...inRange].sort((a, b) => startOf(b) - startOf(a)).map((r) => toRunRow(r, humanRateUsd)),
316
356
  totalRecords: usable.length,
317
357
  };
318
358
  }
319
359
 
320
- export const CSV_COLUMNS = Object.freeze(['startedAt', 'title', 'workflow', 'result', 'costUsd', 'wallMs', 'activeMs', 'reviewCycles', 'prNumber', 'prUrl', 'actor', 'source', 'projects', 'id']);
360
+ export const CSV_COLUMNS = Object.freeze(['startedAt', 'title', 'workflow', 'result', 'costUsd', 'humanHours', 'savedUsd', 'wallMs', 'activeMs', 'pausedMs', 'reviewCycles', 'prNumber', 'prUrl', 'actor', 'source', 'projects', 'id']);
321
361
 
322
362
  function csvCell(v) {
323
363
  if (v == null) return '';
@@ -331,7 +371,7 @@ export function toCsv(runRows) {
331
371
  const lines = [CSV_COLUMNS.join(',')];
332
372
  for (const r of runRows) {
333
373
  lines.push([
334
- r.startedAt, r.title, r.workflow, r.result, r.usd, r.wallMs, r.activeMs, r.reviewCycles,
374
+ r.startedAt, r.title, r.workflow, r.result, r.usd, r.humanHours, r.savedUsd, r.wallMs, r.activeMs, r.pausedMs, r.reviewCycles,
335
375
  r.pr?.number, r.pr?.url, r.actor, r.source, (r.projects || []).join(' '), r.id,
336
376
  ].map(csvCell).join(','));
337
377
  }
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // scripts/install.mjs
2
+ // tools/install.mjs
3
3
  //
4
4
  // Copy the orchestrator agents and the /worca skill into a target project's
5
5
  // .claude directory so that opening Claude Code there lets the user run:
6
6
  // /worca <prompt>
7
7
  //
8
8
  // Usage:
9
- // node scripts/install.mjs <targetDir> [--force]
9
+ // node tools/install.mjs <targetDir> [--force]
10
10
  //
11
11
  // - agents/*.md -> <targetDir>/.claude/agents/
12
12
  // - skills/worca/** -> <targetDir>/.claude/skills/worca/
@@ -39,7 +39,7 @@ function parseArgs(argv) {
39
39
  const HELP = `install — copy orchestrator agents + /worca skill into a project
40
40
 
41
41
  Usage:
42
- node scripts/install.mjs <targetDir> [--force]
42
+ node tools/install.mjs <targetDir> [--force]
43
43
 
44
44
  Copies:
45
45
  agents/*.md -> <targetDir>/.claude/agents/