@worca/app 1.0.0 → 1.2.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 +30 -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 +386 -56
- 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 +199 -23
- package/src/core/ask/attachment-kind.mjs +95 -0
- package/src/core/ask/catalog.mjs +111 -0
- package/src/core/ask/comment-deps.mjs +55 -0
- package/src/core/ask/events.mjs +545 -0
- package/src/core/ask/follow.mjs +113 -0
- package/src/core/ask/git-allowlist.mjs +226 -0
- package/src/core/ask/limits.mjs +57 -0
- package/src/core/ask/mcp-stdio.mjs +135 -0
- package/src/core/ask/models.mjs +125 -0
- package/src/core/ask/prompt.mjs +286 -0
- package/src/core/ask/proposal.mjs +170 -0
- package/src/core/ask/redact.mjs +30 -0
- package/src/core/ask/spawn.mjs +156 -0
- package/src/core/ask/store.mjs +438 -0
- package/src/core/ask/tool-deps.mjs +87 -0
- package/src/core/ask/tools.mjs +879 -0
- package/src/core/ask/turn.mjs +462 -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 +28 -7
- package/src/core/chat/notifier.mjs +6 -1
- package/src/core/chat/renderers.mjs +15 -8
- package/src/core/claude-runner.mjs +541 -62
- package/src/core/config.mjs +310 -44
- package/src/core/cost-budget.mjs +29 -2
- package/src/core/db.mjs +773 -53
- 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/failure-policy.mjs +201 -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 +1072 -0
- package/src/core/graph/seed-templates.mjs +318 -0
- package/src/core/host-guard.mjs +271 -0
- package/src/core/model-env.mjs +180 -8
- package/src/core/model-test.mjs +79 -0
- package/src/core/orchestrator.mjs +994 -4097
- 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 +80 -17
- 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 +3934 -0
- package/src/core/run-manifest.mjs +5 -1
- package/src/core/settings.mjs +184 -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 +4240 -1682
- package/ui/public/ask-markdown.mjs +145 -0
- package/ui/public/ask-model.mjs +317 -0
- package/ui/public/ask-panel.mjs +2129 -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 +311 -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 +1487 -229
- package/ui/public/syntax-highlight.mjs +270 -0
- package/ui/public/thinking-orb.mjs +110 -0
- package/ui/server.mjs +1894 -104
- 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
package/src/core/sources.mjs
CHANGED
|
@@ -7,18 +7,34 @@
|
|
|
7
7
|
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
9
|
import { readFileSync } from 'node:fs';
|
|
10
|
-
import {
|
|
10
|
+
import { join } from 'node:path';
|
|
11
11
|
import { callSource } from './plugin-shim.mjs';
|
|
12
12
|
import { readPluginsLock, pluginCurrentDir } from './plugins-lock.mjs';
|
|
13
|
+
import { listProfiles, DEFAULT_PROFILE } from './plugin-config.mjs';
|
|
13
14
|
import { normalizeManifest } from './plugin-manifest.mjs';
|
|
14
15
|
import { getDb } from './db.mjs';
|
|
15
|
-
import { runDirForRow, readStoreMeta } from './artifacts.mjs';
|
|
16
|
+
import { runDirForRow, readStoreMeta, readPromptFile } from './artifacts.mjs';
|
|
16
17
|
import { RESULTS_FILE } from './results.mjs';
|
|
17
18
|
import { hasGh, findPrForBranch } from './git-info.mjs';
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/** Profile roster for a plugin; never throws — the pane must render even when a
|
|
21
|
+
* plugin's data dir is unreadable (it degrades to "no profiles yet"). */
|
|
22
|
+
function safeProfiles(name) {
|
|
23
|
+
try { return listProfiles(name); } catch { return []; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether an installed source declares multiProfile; never throws (an
|
|
27
|
+
* uninstalled plugin or broken manifest answers false — the callers below
|
|
28
|
+
* degrade to pre-profiles behavior, which is the only thing they could do). */
|
|
29
|
+
function isMultiProfileSource(plugin, sourceId) {
|
|
30
|
+
try {
|
|
31
|
+
const dir = pluginCurrentDir(plugin);
|
|
32
|
+
const norm = normalizeManifest(JSON.parse(readFileSync(join(dir, 'worca-cc-plugin.json'), 'utf8')), { dir });
|
|
33
|
+
if (!norm.ok) return false;
|
|
34
|
+
return (norm.manifest.taskSources || []).some((s) => s.id === sourceId && s.multiProfile === true);
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
/**
|
|
@@ -50,6 +66,12 @@ export function listTaskSources() {
|
|
|
50
66
|
sourceId: ts.id,
|
|
51
67
|
displayName: ts.displayName || `${name}/${ts.id}`,
|
|
52
68
|
inputs: ts.inputs || [],
|
|
69
|
+
// A multi-profile source cannot be used until the pane knows WHICH
|
|
70
|
+
// configuration to run against, so the roster ships with the listing —
|
|
71
|
+
// the New Pipeline pane resolves the project's binding against it
|
|
72
|
+
// without a second round trip.
|
|
73
|
+
multiProfile: ts.multiProfile === true,
|
|
74
|
+
profiles: ts.multiProfile === true ? safeProfiles(name) : [],
|
|
53
75
|
});
|
|
54
76
|
}
|
|
55
77
|
}
|
|
@@ -68,9 +90,9 @@ function taskPromptText(task) {
|
|
|
68
90
|
/**
|
|
69
91
|
* Resolve a source descriptor to the pipeline's task input.
|
|
70
92
|
* { type:'prompt', prompt } | { type:'markdown', promptText?, promptFile? }
|
|
71
|
-
* | { type:'plugin', plugin, sourceId, taskId, inputs? }
|
|
93
|
+
* | { type:'plugin', plugin, sourceId, taskId, inputs?, profile? }
|
|
72
94
|
* @returns {Promise<{promptText:string, promptFile:string|null,
|
|
73
|
-
* sourceMeta:{plugin,sourceId,taskId,url,title}|null}>}
|
|
95
|
+
* sourceMeta:{plugin,sourceId,taskId,profile,url,title}|null}>}
|
|
74
96
|
*/
|
|
75
97
|
export async function resolveTaskInput(source, { projectDir } = {}) {
|
|
76
98
|
const src = source && typeof source === 'object' ? source : { type: 'prompt', prompt: '' };
|
|
@@ -82,19 +104,21 @@ export async function resolveTaskInput(source, { projectDir } = {}) {
|
|
|
82
104
|
|
|
83
105
|
if (type === 'markdown') {
|
|
84
106
|
if (src.promptFile) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
107
|
+
// A NAMED file that cannot be read is an ERROR, never an empty prompt: the
|
|
108
|
+
// old catch{} degradation here (and the identical one in createPipeline) ran
|
|
109
|
+
// a whole pipeline on "" and exited 0. Throws PROMPT_FILE_UNREADABLE — the
|
|
110
|
+
// CLI fail()s on it and ui/server.mjs answers 400.
|
|
111
|
+
const promptText = await readPromptFile(projectDir, src.promptFile);
|
|
91
112
|
return { promptText, promptFile: src.promptFile, sourceMeta: null };
|
|
92
113
|
}
|
|
93
114
|
return { promptText: typeof src.promptText === 'string' ? src.promptText : '', promptFile: null, sourceMeta: null };
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
if (type === 'plugin') {
|
|
97
|
-
const task = await callSource({
|
|
118
|
+
const task = await callSource({
|
|
119
|
+
plugin: src.plugin, sourceId: src.sourceId, op: 'getTask',
|
|
120
|
+
args: { id: src.taskId }, profile: src.profile,
|
|
121
|
+
});
|
|
98
122
|
if (!task) {
|
|
99
123
|
throw new Error(`task-source ${src.plugin}/${src.sourceId}: task "${src.taskId}" not found`);
|
|
100
124
|
}
|
|
@@ -105,6 +129,14 @@ export async function resolveTaskInput(source, { projectDir } = {}) {
|
|
|
105
129
|
plugin: src.plugin,
|
|
106
130
|
sourceId: src.sourceId,
|
|
107
131
|
taskId: src.taskId,
|
|
132
|
+
// Pinned on the ROW, not re-derived at write-back time: the project's
|
|
133
|
+
// binding may have moved to another tracker by then, and a result must
|
|
134
|
+
// always be reported to the instance the task actually came from.
|
|
135
|
+
profile: src.profile || null,
|
|
136
|
+
// The source-panel inputs at fetch time, pinned for the same reason:
|
|
137
|
+
// a per-RUN choice (e.g. jira-source's "Write result back") must gate
|
|
138
|
+
// this run's report with what the user picked when they started it.
|
|
139
|
+
inputs: src.inputs && typeof src.inputs === 'object' ? src.inputs : null,
|
|
108
140
|
url: task.url ?? null,
|
|
109
141
|
title: task.title ?? null,
|
|
110
142
|
},
|
|
@@ -164,31 +196,61 @@ export async function reportResultForPipeline(pipelineRow, resultsBundle) {
|
|
|
164
196
|
try { ref = pipelineRow.source_ref ? JSON.parse(pipelineRow.source_ref) : null; } catch { ref = null; }
|
|
165
197
|
if (!ref?.plugin || !ref?.sourceId || !ref?.taskId) return { ok: true, skipped: true };
|
|
166
198
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
199
|
+
// Which profile to report against. Normally the one pinned on the row when
|
|
200
|
+
// the task was fetched (resolveTaskInput). A row that PREDATES profiles has
|
|
201
|
+
// none — and if the plugin has since upgraded to multiProfile, refusing to
|
|
202
|
+
// report would strand the row forever (no code path can attach a profile to
|
|
203
|
+
// an existing source_ref, so the results-view retry would fail identically
|
|
204
|
+
// every time). Its task came from the instance whose flat config migrated
|
|
205
|
+
// into the DEFAULT_PROFILE bucket, so that is exactly where the report
|
|
206
|
+
// belongs — allowLegacyDefault is the shim's host-internal opt-in for it.
|
|
207
|
+
const profile = ref.profile || null;
|
|
208
|
+
const allowLegacyDefault = !profile && isMultiProfileSource(ref.plugin, ref.sourceId);
|
|
209
|
+
const callRef = {
|
|
210
|
+
plugin: ref.plugin,
|
|
211
|
+
sourceId: ref.sourceId,
|
|
212
|
+
profile: allowLegacyDefault ? DEFAULT_PROFILE : profile,
|
|
213
|
+
allowLegacyDefault,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Capability probe. capabilities() is optional in the connector contract
|
|
217
|
+
// (§7.1: "defaults: writeBack true"): a connector without the op makes the
|
|
218
|
+
// child answer kind 'unimplemented' -> we default to writeBack:true.
|
|
219
|
+
// The run's pinned source-panel inputs travel to BOTH ops so a connector
|
|
220
|
+
// can make write-back a per-run choice: the opt-out rides the reserved
|
|
221
|
+
// input key `writeBack` (jira-source's "Write result back" input;
|
|
222
|
+
// capabilities({inputs}) answers for THIS run).
|
|
223
|
+
// Every OTHER probe error — an implemented capabilities() that crashed, a
|
|
224
|
+
// transport failure (auth/network/timeout/protocol/rate-limit) — means the
|
|
225
|
+
// connector could not answer, and what happens next depends on whether an
|
|
226
|
+
// answer could have mattered: a run that pinned no `writeBack` input fails
|
|
227
|
+
// OPEN (default writeBack:true — a transient rate-limit on the probe must
|
|
228
|
+
// not silently drop the ticket comment when the report itself would have
|
|
229
|
+
// succeeded), while a run WITH one fails CLOSED (the opt-out may be 'no'
|
|
230
|
+
// and we could not hear it; writing would violate it) — the caller
|
|
231
|
+
// surfaces the error and the results-view retry re-probes.
|
|
232
|
+
const inputs = ref.inputs && typeof ref.inputs === 'object' ? ref.inputs : {};
|
|
174
233
|
let writeBack = true;
|
|
175
234
|
try {
|
|
176
|
-
const caps = await callSource({
|
|
235
|
+
const caps = await callSource({ ...callRef, op: 'capabilities', args: { inputs } });
|
|
177
236
|
if (caps && caps.writeBack === false) writeBack = false;
|
|
178
|
-
} catch {
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if ((err?.kind || 'plugin') !== 'unimplemented' && 'writeBack' in inputs) {
|
|
239
|
+
return { ok: false, error: `write-back capability probe failed: ${err?.message || String(err)}` };
|
|
240
|
+
}
|
|
179
241
|
writeBack = true;
|
|
180
242
|
}
|
|
181
243
|
if (!writeBack) return { ok: true, skipped: true };
|
|
182
244
|
|
|
183
245
|
await callSource({
|
|
184
|
-
|
|
185
|
-
sourceId: ref.sourceId,
|
|
246
|
+
...callRef,
|
|
186
247
|
op: 'reportResult',
|
|
187
248
|
args: {
|
|
188
249
|
id: ref.taskId,
|
|
189
250
|
status: statusToResult(pipelineRow.status),
|
|
190
251
|
summary: buildResultSummary(pipelineRow, resultsBundle),
|
|
191
252
|
links: buildResultLinks(resultsBundle),
|
|
253
|
+
inputs,
|
|
192
254
|
},
|
|
193
255
|
});
|
|
194
256
|
return { ok: true };
|
package/src/core/stats.mjs
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// archive exists so these numbers survive history cleanup (spec §6.9). Pure
|
|
4
4
|
// synchronous DB reads; no gh, no git, offline-correct.
|
|
5
5
|
//
|
|
6
|
-
// Attribution: money = cost_ledger by event timestamp
|
|
7
|
-
// and PRs = cohort by started_at (a run belongs to the
|
|
8
|
-
// in, with its CURRENT status). 'today' differs: its totals count runs whose
|
|
6
|
+
// Attribution: money = cost_ledger + ask_cost_ledger by event timestamp
|
|
7
|
+
// (exact); runs, time, and PRs = cohort by started_at (a run belongs to the
|
|
8
|
+
// bucket its start falls in, with its CURRENT status). 'today' differs: its totals count runs whose
|
|
9
9
|
// lifespan [started_at, updated_at] overlaps the day ("active today"), and its
|
|
10
10
|
// hourly bars attribute terminal outcomes to the hour of the terminal write
|
|
11
11
|
// (updated_at). 'all' money/time use the fallback-aware pipelines sums so
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { prepare } from './db.mjs';
|
|
15
15
|
import {
|
|
16
16
|
budgetStatus, costWindowStart, costWindowEnd, allTimeTotals, roundUsd,
|
|
17
|
+
askWindowedSpendUsd,
|
|
17
18
|
} from './cost-budget.mjs';
|
|
18
19
|
|
|
19
20
|
const RANGES = ['today', 'week', 'month', 'all'];
|
|
@@ -84,6 +85,18 @@ function ledgerSpend(fromMs, toMs) {
|
|
|
84
85
|
return roundUsd(row?.s || 0);
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/** Ask Worca window aggregate (ask-cost-statistics-design.md D5/D6): spend,
|
|
89
|
+
* DISTINCT sessions, turns. A thread is a session in every window it has a
|
|
90
|
+
* costed turn in. */
|
|
91
|
+
function askTotals(fromMs, toMs) {
|
|
92
|
+
const row = prepare(`
|
|
93
|
+
SELECT COALESCE(SUM(amount_usd), 0) AS s,
|
|
94
|
+
COUNT(DISTINCT thread_id) AS sessions,
|
|
95
|
+
COUNT(*) AS turns
|
|
96
|
+
FROM ask_cost_ledger WHERE ts >= ? AND ts < ?`).get(fromMs, toMs);
|
|
97
|
+
return { spendUsd: roundUsd(row?.s || 0), sessions: row?.sessions || 0, turns: row?.turns || 0 };
|
|
98
|
+
}
|
|
99
|
+
|
|
87
100
|
/** Build zero-filled buckets [{startMs, endMs}] from windowStart through `now`. */
|
|
88
101
|
function buildBuckets(windowStart, now, bucket) {
|
|
89
102
|
const out = [];
|
|
@@ -137,8 +150,11 @@ export function getStats({ range = 'month', now = new Date() } = {}) {
|
|
|
137
150
|
const windowTotalsFn = range === 'today' ? activeTotals : cohortTotals;
|
|
138
151
|
const shapeTotals = (fromMs, toMs) => {
|
|
139
152
|
const c = windowTotalsFn(fromMs, toMs);
|
|
153
|
+
const pipelineSpendUsd = ledgerSpend(fromMs, toMs);
|
|
154
|
+
const ask = askTotals(fromMs, toMs);
|
|
140
155
|
return {
|
|
141
|
-
spentUsd:
|
|
156
|
+
spentUsd: roundUsd(pipelineSpendUsd + ask.spendUsd),
|
|
157
|
+
pipelineSpendUsd, ask,
|
|
142
158
|
workedMs: c.workedMs,
|
|
143
159
|
runs: c.runs, finished: c.finished, stopped: c.stopped, failed: c.failed,
|
|
144
160
|
paused: c.paused, running: c.running,
|
|
@@ -150,8 +166,11 @@ export function getStats({ range = 'month', now = new Date() } = {}) {
|
|
|
150
166
|
if (range === 'all') {
|
|
151
167
|
const at = allTimeTotals();
|
|
152
168
|
const c = cohortTotals(0, windowEnd.getTime());
|
|
169
|
+
const ask = askTotals(0, windowEnd.getTime());
|
|
153
170
|
totals = {
|
|
154
|
-
spentUsd: at.spendUsd
|
|
171
|
+
spentUsd: roundUsd(at.spendUsd + ask.spendUsd),
|
|
172
|
+
pipelineSpendUsd: at.spendUsd, ask,
|
|
173
|
+
workedMs: at.activeMs,
|
|
155
174
|
runs: c.runs, finished: c.finished, stopped: c.stopped, failed: c.failed,
|
|
156
175
|
paused: c.paused, running: c.running,
|
|
157
176
|
prsOpened: c.prsOpened, prsMerged: c.prsMerged,
|
|
@@ -169,7 +188,7 @@ export function getStats({ range = 'month', now = new Date() } = {}) {
|
|
|
169
188
|
const c = bucketTotalsFn(startMs, endMs);
|
|
170
189
|
return {
|
|
171
190
|
bucketStartMs: startMs,
|
|
172
|
-
spentUsd: ledgerSpend(startMs, endMs),
|
|
191
|
+
spentUsd: roundUsd(ledgerSpend(startMs, endMs) + askWindowedSpendUsd(startMs, endMs)),
|
|
173
192
|
finished: c.finished, stopped: c.stopped, failed: c.failed,
|
|
174
193
|
};
|
|
175
194
|
});
|
package/src/core/title.mjs
CHANGED
|
@@ -11,6 +11,8 @@ const SYSTEM = [
|
|
|
11
11
|
'You write a SHORT, human-readable title for a software task.',
|
|
12
12
|
'Rules: 3–8 words, Title Case-ish, no trailing period, no quotes, no markdown,',
|
|
13
13
|
'no preamble. Output ONLY the title on a single line.',
|
|
14
|
+
'Never ask questions, never ask for more context, never apologize or explain.',
|
|
15
|
+
'If the task is vague or conversational, still output your best-guess topic title.',
|
|
14
16
|
].join(' ');
|
|
15
17
|
|
|
16
18
|
/** Normalize raw model output into a safe single-line title (pure, exported for tests). */
|
|
@@ -21,14 +23,45 @@ export function sanitizeTitle(raw) {
|
|
|
21
23
|
t = t.replace(/^(?:title|task)\s*[:\-]\s*/i, ''); // strip "Title:"/"Task:" label
|
|
22
24
|
t = t.replace(/^["'“”`]+|["'“”`]+$/g, '').trim(); // strip wrapping quotes/backticks
|
|
23
25
|
t = t.replace(/\s+/g, ' ').replace(/\.+$/, '').trim(); // collapse ws, drop trailing dots
|
|
24
|
-
|
|
26
|
+
if (t.length > MAX_LEN) {
|
|
27
|
+
// Cut at a word boundary instead of mid-word ("…you'd li"). The window is
|
|
28
|
+
// MAX_LEN+1 so a word ending exactly at the cap survives; a boundary too
|
|
29
|
+
// close to the start (or none at all — one unbroken run) hard-slices.
|
|
30
|
+
const cut = t.slice(0, MAX_LEN + 1);
|
|
31
|
+
const sp = cut.lastIndexOf(' ');
|
|
32
|
+
t = (sp >= 30 ? cut.slice(0, sp) : t.slice(0, MAX_LEN)).replace(/[\s,;:–—-]+$/, '');
|
|
33
|
+
}
|
|
34
|
+
return t.trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A title that is really a clarifying question / refusal ("I need more context
|
|
38
|
+
// to write a title…"). First-person or plea openers, a trailing question mark,
|
|
39
|
+
// or prose far beyond the 3–8-word instruction all mean the model did not
|
|
40
|
+
// title the task — the caller should keep its provisional title instead.
|
|
41
|
+
// Narrow on purpose (review of PR #376): bare `what`/`which`/`unable`/`please`
|
|
42
|
+
// openers dropped legitimate titles ("What's New Page Redesign", "Which Tab Is
|
|
43
|
+
// Active Indicator", "Unable To Login Error Fix", "Please Wait Spinner Timing").
|
|
44
|
+
// A question is caught by its trailing `?` above; `unable`/`please` count only
|
|
45
|
+
// with the verbs a refusal actually uses.
|
|
46
|
+
const REFUSAL_OPENER_RE =
|
|
47
|
+
/^(?:i(?:['’](?:m|d|ll|ve))?\s|sorry\b|apolog|unfortunately\b|unable to (?:determine|identify|generate|create|provide|write|title|summari[sz]e|infer|tell|help|assist)\b|please (?:provide|share|clarify|give|specify|tell|include|describe|let me know)\b|could you\b|can you\b|tell me\b|what(?:['’]s)? (?:is|are|was|were|do|does|did|would|should|task|work)\b|which (?:task|one|of)\b|help me\b)/i;
|
|
48
|
+
const MAX_WORDS = 12;
|
|
49
|
+
|
|
50
|
+
/** True when sanitized model output looks like a refusal/question, not a title (pure, exported for tests). */
|
|
51
|
+
export function isRefusalTitle(t) {
|
|
52
|
+
if (!t || typeof t !== 'string') return false;
|
|
53
|
+
if (/\?$/.test(t)) return true;
|
|
54
|
+
if (REFUSAL_OPENER_RE.test(t)) return true;
|
|
55
|
+
return t.split(' ').length > MAX_WORDS;
|
|
25
56
|
}
|
|
26
57
|
|
|
27
58
|
/**
|
|
28
59
|
* Produce a concise LLM title for a prompt. Never throws — returns '' on any
|
|
29
60
|
* failure/abort/empty input so the caller keeps the provisional title.
|
|
61
|
+
* `permissionMode` (default 'acceptEdits') exists for Ask Worca: its title call
|
|
62
|
+
* passes 'dontAsk' so the mock dispatcher can never reach a file-writing role.
|
|
30
63
|
* @param {string} prompt
|
|
31
|
-
* @param {{cwd:string, signal?:AbortSignal, model?:string, bin?:string, envScrub?:boolean, envAllowlist?:string[]}} opts
|
|
64
|
+
* @param {{cwd:string, signal?:AbortSignal, model?:string, bin?:string, mock?:boolean, envScrub?:boolean, envAllowlist?:string[], tools?:string[], strictMcpConfig?:boolean, settingSources?:string[], disableSlashCommands?:boolean, mcpConfigPath?:string, permissionMode?:string}} opts
|
|
32
65
|
* @returns {Promise<string>}
|
|
33
66
|
*/
|
|
34
67
|
export async function generateTitle(prompt, opts = {}) {
|
|
@@ -45,18 +78,32 @@ export async function generateTitle(prompt, opts = {}) {
|
|
|
45
78
|
// env everywhere the id is used.
|
|
46
79
|
modelEnv: resolveModelEnv(opts.model || DEFAULT_TITLE_MODEL),
|
|
47
80
|
effort: 'low',
|
|
48
|
-
permissionMode: 'acceptEdits',
|
|
81
|
+
permissionMode: opts.permissionMode || 'acceptEdits',
|
|
49
82
|
allowedTools: [], // empty → no --allowedTools flag → claude defaults; pure text gen
|
|
50
83
|
signal: opts.signal,
|
|
51
84
|
// Title generation runs DURING a pipeline run, so it must honor the same
|
|
52
85
|
// guardrails as the run itself. All three are undefined when absent, and
|
|
53
86
|
// runClaude treats undefined as "not passed" — existing callers are unchanged.
|
|
54
87
|
bin: opts.bin,
|
|
88
|
+
// A run configured with claude:{mock:true} (tests, `--mock`) must title
|
|
89
|
+
// through runMock too: runClaude decides mock-vs-real from THIS field or
|
|
90
|
+
// WORCA_MOCK, and the 29 test files that only pass the option spawned the
|
|
91
|
+
// developer's real binary 157x per `npm test` (2026-08-30).
|
|
92
|
+
mock: opts.mock,
|
|
55
93
|
envScrub: opts.envScrub,
|
|
56
94
|
envAllowlist: opts.envAllowlist,
|
|
95
|
+
// Ask Worca (ask-worca-design.md §6.8): sandbox hardening pass-through for the
|
|
96
|
+
// chat's background title call. All undefined for every existing caller, and
|
|
97
|
+
// runClaude emits nothing for undefined — pipeline title argv is unchanged.
|
|
98
|
+
tools: opts.tools,
|
|
99
|
+
strictMcpConfig: opts.strictMcpConfig,
|
|
100
|
+
settingSources: opts.settingSources,
|
|
101
|
+
disableSlashCommands: opts.disableSlashCommands,
|
|
102
|
+
mcpConfigPath: opts.mcpConfigPath,
|
|
57
103
|
onEvent: () => {},
|
|
58
104
|
});
|
|
59
|
-
|
|
105
|
+
const title = sanitizeTitle(out);
|
|
106
|
+
return isRefusalTitle(title) ? '' : title;
|
|
60
107
|
} catch (err) {
|
|
61
108
|
if (err && err.name === 'AbortError') return ''; // run was stopped — caller keeps provisional
|
|
62
109
|
return '';
|