ensemble-review 1.0.35 → 1.0.36

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 (129) hide show
  1. package/package.json +5 -2
  2. package/dist/ai/check-tool.js +0 -6
  3. package/dist/api/routes/auth.js +0 -194
  4. package/dist/api/routes/billing.js +0 -209
  5. package/dist/api/routes/claude-review.js +0 -522
  6. package/dist/api/routes/contact.js +0 -318
  7. package/dist/api/routes/feed.js +0 -21
  8. package/dist/api/routes/inbox.js +0 -45
  9. package/dist/api/routes/mentions.js +0 -22
  10. package/dist/api/routes/teams.js +0 -150
  11. package/dist/api/routes/webhooks.js +0 -181
  12. package/dist/coding/activity.js +0 -80
  13. package/dist/coding/agent.js +0 -147
  14. package/dist/coding/ai-fix/checks-preview.js +0 -58
  15. package/dist/coding/ai-fix/comment-seed.js +0 -519
  16. package/dist/coding/ai-fix/prompts.js +0 -283
  17. package/dist/coding/ai-resolve/prompt.js +0 -161
  18. package/dist/coding/ai-resolve/routes.js +0 -97
  19. package/dist/coding/ai-resolve/run.js +0 -343
  20. package/dist/coding/ai-resolve/schema.js +0 -26
  21. package/dist/coding/ai-resolve/validate.js +0 -136
  22. package/dist/coding/checks/deny-scan.js +0 -75
  23. package/dist/coding/checks/discover.js +0 -541
  24. package/dist/coding/checks/docker.js +0 -38
  25. package/dist/coding/checks/env.js +0 -88
  26. package/dist/coding/checks/hooks.js +0 -91
  27. package/dist/coding/checks/index.js +0 -8
  28. package/dist/coding/checks/local.js +0 -40
  29. package/dist/coding/checks/sandbox.js +0 -206
  30. package/dist/coding/checks/session.js +0 -518
  31. package/dist/coding/checks/spawn.js +0 -123
  32. package/dist/coding/checks/spec.js +0 -140
  33. package/dist/coding/checks/types.js +0 -6
  34. package/dist/coding/partial-spend.js +0 -19
  35. package/dist/coding/safe-path.js +0 -75
  36. package/dist/coding/story-build/build-prompts.js +0 -178
  37. package/dist/coding/story-build/build.js +0 -332
  38. package/dist/coding/story-build/checkouts.js +0 -66
  39. package/dist/coding/story-build/docs-runner.js +0 -178
  40. package/dist/coding/story-build/docs.js +0 -334
  41. package/dist/coding/story-build/manager.js +0 -999
  42. package/dist/coding/story-build/plan-agent.js +0 -190
  43. package/dist/coding/story-build/plan.js +0 -338
  44. package/dist/coding/story-build/prompts.js +0 -147
  45. package/dist/coding/story-build/routes-docs.js +0 -113
  46. package/dist/coding/story-build/routes.js +0 -295
  47. package/dist/coding/story-build/schema.js +0 -53
  48. package/dist/db/automation-output.js +0 -349
  49. package/dist/db/credits.js +0 -79
  50. package/dist/db/schema.js +0 -321
  51. package/dist/db/schema.pg.js +0 -2197
  52. package/dist/db/sprint-repo-suggest.js +0 -257
  53. package/dist/db/sprint-stories.js +0 -42
  54. package/dist/db/sprint-story.js +0 -505
  55. package/dist/db/sprint-ticket-repos.js +0 -195
  56. package/dist/db/story-selection.js +0 -270
  57. package/dist/db/team-comparison.js +0 -124
  58. package/dist/db/workspace-comparison.js +0 -98
  59. package/dist/feed/fyi-provider.js +0 -10
  60. package/dist/github/local-user.js +0 -92
  61. package/dist/github/repo-files.js +0 -26
  62. package/dist/ml/worker.js +0 -31
  63. package/dist/platform/advisor/discovery/tier2.js +0 -88
  64. package/dist/platform/advisor/discovery/tier3.js +0 -57
  65. package/dist/platform/advisor/emitters/adapter.js +0 -5
  66. package/dist/platform/advisor/emitters/coderabbit.js +0 -175
  67. package/dist/platform/advisor/emitters/copilot-prose.js +0 -118
  68. package/dist/platform/advisor/emitters/greptile.js +0 -176
  69. package/dist/platform/advisor/emitters/lint.js +0 -37
  70. package/dist/platform/advisor/emitters/manifest.js +0 -239
  71. package/dist/platform/advisor/emitters/prose.js +0 -183
  72. package/dist/platform/advisor/emitters/registry.js +0 -20
  73. package/dist/platform/advisor/emitters/yaml-doc.js +0 -95
  74. package/dist/platform/advisor/evidence.js +0 -79
  75. package/dist/platform/advisor/findings.js +0 -15
  76. package/dist/platform/advisor/intents.js +0 -269
  77. package/dist/platform/advisor/measure/effect.js +0 -18
  78. package/dist/platform/advisor/outputs/brief.js +0 -68
  79. package/dist/platform/advisor/outputs/config-pr.js +0 -156
  80. package/dist/platform/advisor/refine/diff-guard.js +0 -38
  81. package/dist/platform/advisor/refine/refine.js +0 -35
  82. package/dist/platform/advisor/retro-check.js +0 -98
  83. package/dist/platform/advisor/routes.js +0 -648
  84. package/dist/platform/advisor/store.js +0 -321
  85. package/dist/platform/advisor/taxonomy.js +0 -67
  86. package/dist/platform/annotations/prompts.js +0 -84
  87. package/dist/platform/annotations/routes.js +0 -86
  88. package/dist/platform/annotations/runner.js +0 -234
  89. package/dist/platform/annotations/targets.js +0 -203
  90. package/dist/platform/insights/grounded-figures.js +0 -162
  91. package/dist/platform/insights/period-questions.js +0 -235
  92. package/dist/platform/work-plan/hash.js +0 -99
  93. package/dist/platform/work-plan/prompt.js +0 -209
  94. package/dist/platform/work-plan/routes.js +0 -350
  95. package/dist/pro/bind.js +0 -309
  96. package/dist/pro/contract.js +0 -36
  97. package/dist/pro/event-hooks.js +0 -37
  98. package/dist/pro/migrate.js +0 -64
  99. package/dist/review/events.js +0 -24
  100. package/dist/review/local-settings.js +0 -78
  101. package/dist/review/memory/capture.js +0 -0
  102. package/dist/review/memory/retrieval.js +0 -272
  103. package/dist/review/memory/routes.js +0 -78
  104. package/dist/review/persist.js +0 -203
  105. package/dist/review/plugin-providers.js +0 -12
  106. package/dist/review/prompt.js +0 -264
  107. package/dist/review/review-manager.js +0 -259
  108. package/dist/review/routing.js +0 -164
  109. package/dist/review/sprint-assess/agent.js +0 -145
  110. package/dist/review/sprint-assess/manager.js +0 -382
  111. package/dist/review/sprint-assess/prompts.js +0 -99
  112. package/dist/review/sprint-assess/routes.js +0 -109
  113. package/dist/review/sprint-assess/schema.js +0 -70
  114. package/dist/review/sprint-assess/validate.js +0 -180
  115. package/dist/review/sprint-story/manager.js +0 -98
  116. package/dist/review/sprint-story/prompt.js +0 -45
  117. package/dist/review/sprint-story/validate.js +0 -127
  118. package/dist/review/story-docs/build-prompts.js +0 -153
  119. package/dist/review/story-docs/build-validate.js +0 -162
  120. package/dist/review/story-report/agent.js +0 -203
  121. package/dist/review/story-report/fingerprint.js +0 -45
  122. package/dist/review/story-report/manager.js +0 -287
  123. package/dist/review/story-report/routes.js +0 -154
  124. package/dist/review/story-report/story-detail.js +0 -148
  125. package/dist/review/story-report/validate.js +0 -105
  126. package/dist/review/ticket-review/sweep-waits.js +0 -20
  127. package/dist/review/write-deny.js +0 -170
  128. package/dist/shared/conflict-ai.js +0 -24
  129. package/dist/sync/benchmark-rollup.js +0 -50
@@ -1,287 +0,0 @@
1
- import { and, desc, eq, inArray } from 'drizzle-orm';
2
- import { STORY_REPORT_MODEL, } from '../../shared/index.js';
3
- import { config } from '../../config.js';
4
- import { db, schema } from '../../db/client.js';
5
- import { loadStorySelection } from '../../db/story-selection.js';
6
- import { pickReviewNonce } from '../claude-review/prompts.js';
7
- import { runStoryLabelsToEnd } from '../story-labels/manager.js';
8
- import { selectionFingerprint } from './fingerprint.js';
9
- import { storyUntrustedTexts } from './story-detail.js';
10
- import { validateStoryReport } from './validate.js';
11
- let labelSeams = null;
12
- /** Tests inject the label pass's completion/store seams (never a real model call); null restores them. */
13
- export function setStoryReportLabelSeamsForTests(s) {
14
- labelSeams = s;
15
- }
16
- const slots = new Map(); // accountId → the one in-flight run
17
- const runs = new Map(); // report id → its background run (tests await it)
18
- /** Claim the account's slot. SYNCHRONOUS — call before any await. false = already taken. */
19
- export function claimStoryReportSlot(accountId) {
20
- if (slots.has(accountId))
21
- return false;
22
- slots.set(accountId, { id: null, abort: new AbortController() });
23
- return true;
24
- }
25
- export function releaseStoryReportSlot(accountId) {
26
- slots.delete(accountId);
27
- }
28
- /** The run id holding the account's slot (null while the claim has no row yet, or none). */
29
- export function storyReportSlotId(accountId) {
30
- return slots.get(accountId)?.id ?? null;
31
- }
32
- let runnerOverride = null;
33
- /** Tests inject a fake runner (never a real model call); null restores the SDK runner. */
34
- export function setStoryReportRunnerForTests(fn) {
35
- runnerOverride = fn;
36
- }
37
- /** The background run of `id`, for tests to await; undefined when none. */
38
- export function storyReportRunPromise(id) {
39
- return runs.get(id);
40
- }
41
- export function storyReportRecordOf(r) {
42
- return {
43
- id: r.id,
44
- workspaceId: r.workspaceId,
45
- window: r.windowChoice,
46
- range: r.rangeJson,
47
- model: r.model,
48
- status: r.status,
49
- picked: r.pickedJson ?? [],
50
- population: r.populationJson,
51
- report: r.status === 'succeeded' ? (r.reportJson ?? null) : null,
52
- dropped: r.droppedJson ?? null,
53
- error: r.error ?? null,
54
- costUsd: r.costUsd ?? null,
55
- inputTokens: r.inputTokens ?? null,
56
- outputTokens: r.outputTokens ?? null,
57
- numTurns: r.numTurns ?? null,
58
- createdAt: r.createdAt.toISOString(),
59
- completedAt: r.completedAt != null ? r.completedAt.toISOString() : null,
60
- };
61
- }
62
- const t = () => schema.storyReports;
63
- /** One run by id — this account's only (another account's id → null → 404). */
64
- export async function loadStoryReport(accountId, id) {
65
- const rows = await db
66
- .select()
67
- .from(t())
68
- .where(and(eq(t().accountId, accountId), eq(t().id, id)))
69
- .limit(1)
70
- .execute();
71
- return rows[0] ?? null;
72
- }
73
- /** The latest run of any status and the latest SUCCEEDED run for (account, workspace, window). */
74
- export async function latestStoryReports(accountId, workspaceId, choice) {
75
- const where = and(eq(t().accountId, accountId), eq(t().workspaceId, workspaceId), eq(t().windowChoice, choice));
76
- const [latest] = await db.select().from(t()).where(where).orderBy(desc(t().createdAt), desc(t().id)).limit(1).execute();
77
- const [report] = await db
78
- .select()
79
- .from(t())
80
- .where(and(where, eq(t().status, 'succeeded')))
81
- .orderBy(desc(t().createdAt), desc(t().id))
82
- .limit(1)
83
- .execute();
84
- return { latest: latest ?? null, report: report ?? null };
85
- }
86
- /** The account's in-flight run (any workspace / window), or null. */
87
- export async function runningStoryReport(accountId) {
88
- const [r] = await db
89
- .select({ id: t().id, workspaceId: t().workspaceId, windowChoice: t().windowChoice, status: t().status })
90
- .from(t())
91
- .where(and(eq(t().accountId, accountId), inArray(t().status, ['queued', 'running'])))
92
- .orderBy(desc(t().createdAt), desc(t().id))
93
- .limit(1)
94
- .execute();
95
- return r
96
- ? { id: r.id, workspaceId: r.workspaceId, window: r.windowChoice, status: r.status }
97
- : null;
98
- }
99
- /**
100
- * Insert the queued row and launch the run in the background. The caller MUST hold the account's
101
- * slot (`claimStoryReportSlot`); from here the run owns it and releases it when it ends. If the
102
- * insert throws, the caller still owns the slot and releases it.
103
- */
104
- export async function startStoryReport(ctx, a) {
105
- const slot = slots.get(a.accountId);
106
- if (slot == null)
107
- throw new Error('story report: start without a claimed slot');
108
- const [row] = (await db
109
- .insert(t())
110
- .values({
111
- accountId: a.accountId,
112
- workspaceId: a.workspaceId,
113
- windowChoice: a.choice,
114
- fromAt: new Date(a.range.from),
115
- toAt: new Date(a.range.to),
116
- rangeJson: a.range,
117
- model: STORY_REPORT_MODEL,
118
- status: 'queued',
119
- fingerprint: a.fingerprint,
120
- pickedJson: a.candidates,
121
- populationJson: a.population,
122
- createdAt: new Date(),
123
- })
124
- .returning()
125
- .execute());
126
- slot.id = row.id;
127
- const p = runStoryReport(ctx, row.id, a, slot.abort).finally(() => {
128
- runs.delete(row.id);
129
- if (slots.get(a.accountId) === slot)
130
- slots.delete(a.accountId);
131
- });
132
- runs.set(row.id, p);
133
- return storyReportRecordOf(row);
134
- }
135
- /** Map an agent result with no usable report to its error code. */
136
- export function storyReportFailureOf(res) {
137
- if (res.runtimeMissing)
138
- return 'runtime';
139
- if (res.error != null)
140
- return 'failed';
141
- if (res.subtype === 'error_max_turns')
142
- return 'max_turns';
143
- if (res.subtype === 'error_max_budget_usd')
144
- return 'budget_exceeded';
145
- return 'no_report';
146
- }
147
- const windowLabelOf = (range) => range.reporting != null && range.reporting.mode === 'sprint'
148
- ? 'sprint so far'
149
- : `last ${range.reporting?.days ?? range.days} days`;
150
- async function finish(id, set, cas) {
151
- await db
152
- .update(t())
153
- .set({ ...set, completedAt: new Date() })
154
- .where(cas ? and(eq(t().id, id), inArray(t().status, ['queued', 'running'])) : eq(t().id, id))
155
- .execute();
156
- }
157
- async function runStoryReport(ctx, id, a, abort) {
158
- try {
159
- await db
160
- .update(t())
161
- .set({ status: 'running' })
162
- .where(and(eq(t().id, id), eq(t().status, 'queued')))
163
- .execute();
164
- if (abort.signal.aborted)
165
- return; // cancelled before it started; the cancel wrote the row
166
- if (ctx.llm.detectAuth().status !== 'ok') {
167
- await finish(id, { status: 'failed', error: 'auth' }, true);
168
- return;
169
- }
170
- // THE LABEL PASS (header). Best effort: a failed pass leaves the labels as they were.
171
- let labelCostUsd = 0;
172
- let sel = { candidates: a.candidates, population: a.population, internalsByKey: a.internalsByKey };
173
- try {
174
- const pass = await runStoryLabelsToEnd({ ...ctx, db, schema }, {
175
- accountId: a.accountId,
176
- workspaceId: a.workspaceId,
177
- budgetUsd: config.storyLabelBudgetUsd,
178
- stories: async () => (await loadStorySelection(a.accountId, a.scope, a.choice, Date.now())).toLabel,
179
- }, labelSeams ?? {}, abort.signal);
180
- labelCostUsd = pass?.costUsd ?? 0;
181
- if (pass != null && pass.labelled > 0 && !abort.signal.aborted) {
182
- const fresh = await loadStorySelection(a.accountId, a.scope, a.choice, Date.now());
183
- if (fresh.candidates.length > 0) {
184
- sel = { candidates: fresh.candidates, population: fresh.population, internalsByKey: fresh.internalsByKey };
185
- await db
186
- .update(t())
187
- .set({ pickedJson: sel.candidates, populationJson: sel.population, fingerprint: selectionFingerprint(a.choice, sel) })
188
- .where(and(eq(t().id, id), inArray(t().status, ['queued', 'running'])))
189
- .execute();
190
- }
191
- }
192
- }
193
- catch (err) {
194
- ctx.log.warn({ reportId: id, err: String(err).slice(0, 200) }, 'story report: label pass failed (non-fatal)');
195
- }
196
- if (abort.signal.aborted) {
197
- await finish(id, { status: 'cancelled', costUsd: labelCostUsd > 0 ? labelCostUsd : null }, false);
198
- return;
199
- }
200
- const runner = runnerOverride ?? (await import('./agent.js')).runStoryReportAgent;
201
- // The credential ladder is applied to process.env only when no Claude Review run is in flight
202
- // (the review chat's rule): two runs mutating the env at once would restore each other's values.
203
- const { chatMayApplyAuthEnv } = await import('../claude-review/manager.js');
204
- const res = await runner({
205
- candidates: sel.candidates,
206
- population: sel.population,
207
- internalsByKey: sel.internalsByKey,
208
- windowLabel: windowLabelOf(a.range),
209
- model: STORY_REPORT_MODEL,
210
- nonce: pickReviewNonce(storyUntrustedTexts(sel.candidates, sel.internalsByKey)),
211
- maxTurns: config.storyReportMaxTurns,
212
- maxBudgetUsd: config.storyReportBudgetUsd,
213
- applyAuthEnv: chatMayApplyAuthEnv(),
214
- abortController: abort,
215
- });
216
- const usage = {
217
- // The row's cost is the whole run's: the agent's plus the label pass's (each recorded apart).
218
- costUsd: res.costUsd == null && labelCostUsd === 0 ? res.costUsd : (res.costUsd ?? 0) + labelCostUsd,
219
- inputTokens: res.inputTokens,
220
- outputTokens: res.outputTokens,
221
- numTurns: res.numTurns,
222
- };
223
- if (res.costUsd != null && res.costUsd > 0) {
224
- await ctx
225
- .recordAiUsage({
226
- accountId: a.accountId,
227
- seam: 'agent',
228
- feature: 'story_report',
229
- model: STORY_REPORT_MODEL,
230
- costUsd: res.costUsd,
231
- inputTokens: res.inputTokens,
232
- outputTokens: res.outputTokens,
233
- })
234
- .catch((err) => ctx.log.warn({ err }, 'story report: usage record failed'));
235
- }
236
- if (res.aborted || abort.signal.aborted) {
237
- await finish(id, { status: 'cancelled', ...usage }, false);
238
- return;
239
- }
240
- if (res.error != null)
241
- ctx.log.warn({ reportId: id, err: res.error }, 'story report: run failed');
242
- if (res.submitted === undefined) {
243
- await finish(id, { status: 'failed', error: storyReportFailureOf(res), ...usage }, true);
244
- return;
245
- }
246
- const v = validateStoryReport(res.submitted, new Set(sel.candidates.map((c) => c.key)));
247
- if (v.dropped.unknownTickets + v.dropped.items + v.dropped.overCap > 0) {
248
- ctx.log.info({ reportId: id, dropped: v.dropped }, 'story report: validation removed items');
249
- }
250
- if (!v.ok) {
251
- await finish(id, { status: 'failed', error: 'invalid_report', droppedJson: v.dropped, ...usage }, true);
252
- return;
253
- }
254
- await finish(id, { status: 'succeeded', reportJson: v.report, droppedJson: v.dropped, error: null, ...usage }, true);
255
- }
256
- catch (err) {
257
- ctx.log.warn({ err, reportId: id }, 'story report: run threw');
258
- await finish(id, { status: 'failed', error: 'failed' }, true).catch(() => { });
259
- }
260
- }
261
- /**
262
- * Cancel this account's run `id`. In flight → abort and write 'cancelled' now; already finished →
263
- * unchanged. Returns the row after, or null when it is not this account's.
264
- */
265
- export async function cancelStoryReport(accountId, id) {
266
- const row = await loadStoryReport(accountId, id);
267
- if (row == null)
268
- return null;
269
- if (row.status !== 'queued' && row.status !== 'running')
270
- return row;
271
- const slot = slots.get(accountId);
272
- if (slot?.id === id)
273
- slot.abort.abort();
274
- await finish(id, { status: 'cancelled' }, true);
275
- return loadStoryReport(accountId, id);
276
- }
277
- /** Boot sweep: a queued/running row from an earlier process can never finish — mark it interrupted. */
278
- export async function reconcileStoryReportsOnStartup() {
279
- const rows = await db
280
- .update(t())
281
- .set({ status: 'failed', error: 'interrupted', completedAt: new Date() })
282
- .where(inArray(t().status, ['queued', 'running']))
283
- .returning({ id: t().id })
284
- .execute();
285
- return rows.length;
286
- }
287
- //# sourceMappingURL=manager.js.map
@@ -1,154 +0,0 @@
1
- import { STORY_REPORT_WINDOW_CHOICES, parseStoryReportWindow, } from '../../shared/index.js';
2
- import { resolveWorkspaceScope } from '../../db/queries.js';
3
- import { loadStorySelection } from '../../db/story-selection.js';
4
- import { reportsEntitled } from '../../api/routes/insights.js';
5
- import { selectionFingerprint } from './fingerprint.js';
6
- import { cancelStoryReport, claimStoryReportSlot, latestStoryReports, loadStoryReport, releaseStoryReportSlot, runningStoryReport, startStoryReport, storyReportRecordOf, storyReportSlotId, } from './manager.js';
7
- // THE STORY REPORT'S ROUTES (docs/API.md § Story report; docs/CLAUDE-REVIEW.md § Story report).
8
- // Registered ONLY through `registerAgenticRoutes` — its explicit `isCloud` check is the cloud
9
- // guarantee (the routes 404 there). Pro with the rest of Reports: `reportsEntitled` false → 402
10
- // BEFORE any DB work, identical for every id/workspace (no oracle). The workspace always goes
11
- // through `resolveWorkspaceScope`: an unknown or another tenant's id degrades to the caller's
12
- // Default, never a 404, and every response echoes the workspace used.
13
- //
14
- // GET /api/story-reports?workspace=&window= picks + figures NOW (code) + stored runs → read
15
- // POST /api/story-reports {workspaceId, window} the ONLY path that runs the model → ai + ai_hourly
16
- // (the run first LABELS the window's stories with no current label — manager.ts)
17
- // GET /api/story-reports/:id one run (the SPA polls while in flight) → read
18
- // POST /api/story-reports/:id/cancel abort; spends nothing → read
19
- const PRO_REQUIRED = { error: 'pro required' };
20
- const fingerprintOf = (choice, sel) => selectionFingerprint(choice, sel);
21
- const parseId = (raw) => {
22
- const n = typeof raw === 'string' && /^\d+$/.test(raw) ? Number(raw) : NaN;
23
- return Number.isSafeInteger(n) && n > 0 ? n : null;
24
- };
25
- export function registerStoryReportRoutes(app, ctx) {
26
- /** A succeeded run's currency against today's picks for its own workspace + window. */
27
- const currencyOf = async (accountId, row) => {
28
- if (row.status !== 'succeeded')
29
- return null;
30
- const scope = await resolveWorkspaceScope(accountId, row.workspaceId);
31
- const choice = parseStoryReportWindow(row.windowChoice);
32
- const sel = await loadStorySelection(accountId, scope, choice, Date.now());
33
- return fingerprintOf(choice, sel) === row.fingerprint ? 'current' : 'stale';
34
- };
35
- app.get('/api/story-reports', async (req, reply) => {
36
- if (!reportsEntitled(req)) {
37
- reply.status(402);
38
- return PRO_REQUIRED;
39
- }
40
- const q = req.query;
41
- const accountId = ctx.accountIdOf(req);
42
- const scope = await resolveWorkspaceScope(accountId, q.workspace);
43
- const choice = parseStoryReportWindow(q.window);
44
- const sel = await loadStorySelection(accountId, scope, choice, Date.now());
45
- const { latest, report } = await latestStoryReports(accountId, scope.workspaceId, choice);
46
- return {
47
- workspaceId: scope.workspaceId,
48
- window: choice,
49
- range: sel.range,
50
- trackerOn: sel.trackerOn,
51
- latest: latest ? storyReportRecordOf(latest) : null,
52
- report: report ? storyReportRecordOf(report) : null,
53
- currency: report ? (fingerprintOf(choice, sel) === report.fingerprint ? 'current' : 'stale') : null,
54
- running: await runningStoryReport(accountId),
55
- candidates: sel.candidates,
56
- population: sel.population,
57
- };
58
- });
59
- app.post('/api/story-reports', async (req, reply) => {
60
- if (!reportsEntitled(req)) {
61
- reply.status(402);
62
- return PRO_REQUIRED;
63
- }
64
- const body = (req.body ?? {});
65
- if (typeof body.workspaceId !== 'number' ||
66
- !Number.isSafeInteger(body.workspaceId) ||
67
- typeof body.window !== 'string' ||
68
- !STORY_REPORT_WINDOW_CHOICES.includes(body.window)) {
69
- reply.status(400);
70
- return { error: 'bad_request', message: 'Send a workspaceId and a window (reporting, 30, 60 or 90).' };
71
- }
72
- const choice = body.window;
73
- const accountId = ctx.accountIdOf(req);
74
- // ⚠ THE SLOT IS CLAIMED HERE, BEFORE THE FIRST AWAIT — two POSTs cannot both start a run.
75
- if (!claimStoryReportSlot(accountId)) {
76
- // Name only a run whose row is still queued/running. A CANCELLED run keeps the slot until
77
- // its SDK call has actually stopped (the slot also serialises the auth-env mutation, so it
78
- // must not be released early) — its row already reads 'cancelled', so naming it would point
79
- // the card at a run it no longer shows.
80
- const running = await runningStoryReport(accountId);
81
- const stopping = running == null && storyReportSlotId(accountId) != null;
82
- reply.status(409);
83
- return {
84
- error: 'busy',
85
- message: stopping
86
- ? 'The cancelled story report is still stopping. Try again in a moment.'
87
- : 'A story report is already being written.',
88
- ...(running != null ? { runningId: running.id } : {}),
89
- };
90
- }
91
- let handedOff = false;
92
- try {
93
- const scope = await resolveWorkspaceScope(accountId, body.workspaceId);
94
- const sel = await loadStorySelection(accountId, scope, choice, Date.now());
95
- if (!sel.trackerOn) {
96
- reply.status(400);
97
- return { error: 'no_tracker', message: 'This workspace has no issue tracker.' };
98
- }
99
- // Picks never depend on labels (only their issue fields and the rates do), so no picks means
100
- // nothing to report even after a label pass.
101
- if (sel.candidates.length === 0) {
102
- reply.status(422);
103
- return { error: 'no_candidates', message: 'No stories to report on in this window.' };
104
- }
105
- const record = await startStoryReport(ctx, {
106
- accountId,
107
- workspaceId: scope.workspaceId,
108
- scope,
109
- choice,
110
- range: sel.range,
111
- candidates: sel.candidates,
112
- population: sel.population,
113
- internalsByKey: sel.internalsByKey,
114
- fingerprint: fingerprintOf(choice, sel),
115
- });
116
- handedOff = true;
117
- reply.status(202);
118
- return { report: record };
119
- }
120
- finally {
121
- if (!handedOff)
122
- releaseStoryReportSlot(accountId);
123
- }
124
- });
125
- app.get('/api/story-reports/:id', async (req, reply) => {
126
- if (!reportsEntitled(req)) {
127
- reply.status(402);
128
- return PRO_REQUIRED;
129
- }
130
- const id = parseId(req.params.id);
131
- const accountId = ctx.accountIdOf(req);
132
- const row = id != null ? await loadStoryReport(accountId, id) : null;
133
- if (row == null) {
134
- reply.status(404);
135
- return { error: 'not_found' };
136
- }
137
- return { report: storyReportRecordOf(row), currency: await currencyOf(accountId, row) };
138
- });
139
- app.post('/api/story-reports/:id/cancel', async (req, reply) => {
140
- if (!reportsEntitled(req)) {
141
- reply.status(402);
142
- return PRO_REQUIRED;
143
- }
144
- const id = parseId(req.params.id);
145
- const accountId = ctx.accountIdOf(req);
146
- const row = id != null ? await cancelStoryReport(accountId, id) : null;
147
- if (row == null) {
148
- reply.status(404);
149
- return { error: 'not_found' };
150
- }
151
- return { report: storyReportRecordOf(row), currency: await currencyOf(accountId, row) };
152
- });
153
- }
154
- //# sourceMappingURL=routes.js.map
@@ -1,148 +0,0 @@
1
- import { STORY_DEPENDENCY_KIND_LABEL, STORY_ISSUES, STORY_ISSUE_BY_ID, STORY_SPEC_GAP_TYPE_LABEL, } from '../../shared/index.js';
2
- // What the story report's agent READS — the trial harness's (scratchpad haiku/agent-exp.mts)
3
- // `candidateTable`, `population` and `storyDetail()` verbatim, adapted only to the stored shapes. Pure.
4
- //
5
- // ⚠ ALL OF IT IS ATTACKER-AUTHORED TEXT (ticket titles, descriptions and criteria; PR titles; the
6
- // ticket review's model prose). agent.ts nonce-fences every tool answer built here.
7
- const COLOR_RE = /\{color[^}]*\}/g;
8
- /** Drop Jira colour markup, collapse whitespace before a newline, cap. */
9
- export function stripStoryText(s, cap) {
10
- return (s ?? '').replace(COLOR_RE, '').replace(/\s+\n/g, '\n').slice(0, cap);
11
- }
12
- /** The prototype's caps on the ticket text `get_story` returns. */
13
- export const STORY_DETAIL_DESCRIPTION_MAX = 3000;
14
- export const STORY_DETAIL_AC_MAX = 2500;
15
- /** "2.4 working days" / "5.0 working hours" — the agent's duration words. */
16
- export function workingDuration(hours, dayHours) {
17
- return hours >= dayHours ? `${(hours / dayHours).toFixed(1)} working days` : `${hours.toFixed(1)} working hours`;
18
- }
19
- /** `list_candidates`' table row. */
20
- export function candidateRow(c, dayHours) {
21
- return {
22
- key: c.key,
23
- title: c.title,
24
- url: c.url,
25
- group: c.group,
26
- whyPicked: c.whyPicked,
27
- time: workingDuration(c.workingHours, dayHours),
28
- pullRequests: c.pullRequests,
29
- linesChanged: c.linesChanged,
30
- criteria: c.criteria,
31
- criteriaSource: c.criteriaSource,
32
- review: c.review
33
- ? { alignment: c.review.alignment, met: c.review.met, partlyMet: c.review.partlyMet, notMet: c.review.notMet, missing: c.review.missing }
34
- : null,
35
- // The three story issues by LABEL (what the agent quotes), each with a state: present (and the
36
- // specific detail behind it), false = known absent, null = NOT KNOWN. ⚠ An unread ticket is
37
- // `issues: null` and an unlabelled one says null per issue — never `[]` ("avoids all three").
38
- ...(c.issues !== undefined ? { issues: candidateIssues(c) } : {}),
39
- };
40
- }
41
- /** The SPECIFIC detail behind one present issue — which criteria and why, what the story waited on
42
- * (who, ticket, status, finished or not), which details the spec leaves out. Ticket-derived strings:
43
- * fenced with the rest of the tool answer, and in `storyUntrustedTexts`. */
44
- export function issueDetailForAgent(i) {
45
- switch (i.id) {
46
- case 'unclear_done':
47
- return i.reasons.includes('no_criteria')
48
- ? { noCriteria: true }
49
- : { criteria: i.criteria.map((c) => ({ criterion: c.n, reason: c.reason })) };
50
- case 'blocked':
51
- return {
52
- waitsOn: i.blockers.map((b) => ({
53
- what: b.what,
54
- kind: STORY_DEPENDENCY_KIND_LABEL[b.kind],
55
- ticket: b.ticketKey,
56
- owner: b.owner,
57
- status: b.status,
58
- finished: b.resolved,
59
- source: b.source === 'link' ? 'tracker link' : 'named in the ticket text',
60
- })),
61
- };
62
- case 'spec_gaps':
63
- return { gaps: i.gaps.map((g) => ({ type: STORY_SPEC_GAP_TYPE_LABEL[g.type], gap: g.text })) };
64
- }
65
- }
66
- /** One candidate's three issues for the agent; null when the ticket text was never read. Old rows'
67
- * retired ids never reach here (the list is STORY_ISSUES, looked up by id). */
68
- export function candidateIssues(c) {
69
- if (c.issues == null)
70
- return null;
71
- const states = c.issueStates ?? null;
72
- return STORY_ISSUES.map((m) => {
73
- const hit = c.issues.find((i) => i.id === m.id);
74
- if (hit != null)
75
- return { issue: m.label, present: true, ...issueDetailForAgent(hit) };
76
- // An issue code did not raise is known absent only when its state says so; old rows had no state.
77
- return { issue: m.label, present: states != null ? (states[m.id] ?? null) : null };
78
- });
79
- }
80
- /** `list_candidates`' `wholeWindow` — the raw population with the median as words, as the prototype sent. */
81
- export function populationForAgent(p) {
82
- return {
83
- storiesInWindow: p.storiesInWindow,
84
- withWrittenCriteria: p.withWrittenCriteria,
85
- criteriaOnlyInDescription: p.criteriaOnlyInDescription,
86
- noCriteriaFound: p.noCriteriaFound,
87
- checkedByTicketReview: p.checkedByTicketReview,
88
- checkedAndFullyMet: p.checkedAndFullyMet,
89
- checkedWithGaps: p.checkedWithGaps,
90
- medianWorkingTime: workingDuration(p.medianWorkingHours ?? 0, p.workingDayHours),
91
- // Whole-window, by code: the ONLY issue rates the agent may quote, each in its own unit (`count` of
92
- // `of` criteria or stories); no-criteria is `noCriteriaFound`.
93
- ...(p.issueRates != null
94
- ? {
95
- issueRates: p.issueRates.map((r) => ({
96
- measure: STORY_ISSUE_BY_ID[r.id].measureLabel,
97
- count: r.stories,
98
- of: r.of,
99
- unit: r.unit ?? 'stories',
100
- })),
101
- }
102
- : {}),
103
- };
104
- }
105
- /** `get_story` for one picked key; an unknown key gets the prototype's error object. */
106
- export function storyDetail(key, byKey) {
107
- const s = byKey.get(key);
108
- if (!s)
109
- return { error: `Unknown ticket ${key}. Use a key from list_candidates.` };
110
- const snap = s.review?.snapshot, t = s.ticket;
111
- const a = s.review?.assessment;
112
- return {
113
- key,
114
- title: s.row.title,
115
- url: s.row.url,
116
- description: stripStoryText(snap?.description ?? t?.description, STORY_DETAIL_DESCRIPTION_MAX),
117
- acceptanceCriteria: stripStoryText(snap?.acceptanceCriteria ?? t?.acceptanceCriteria, STORY_DETAIL_AC_MAX),
118
- review: a
119
- ? {
120
- summary: a.summary,
121
- criteria: a.criteria.map((x) => ({ ref: x.ref, text: x.text, status: x.status, explanation: x.explanation })),
122
- missing: a.missing.map((m) => ({ ref: m.ref, title: m.title, explanation: m.explanation })),
123
- notRequested: a.notRequested.map((n) => ({ title: n.title })),
124
- }
125
- : null,
126
- pullRequests: [...s.prs.values()].map((p) => ({
127
- ref: `${p.repoFullName.split('/')[1]}#${p.number}`,
128
- title: p.title,
129
- state: p.state,
130
- linesChanged: p.additions + p.deletions,
131
- })),
132
- };
133
- }
134
- /** Every untrusted string a tool answer may carry this run — the nonce-collision scan's input. */
135
- export function storyUntrustedTexts(candidates, byKey) {
136
- const out = [];
137
- for (const c of candidates) {
138
- out.push(c.key, c.title ?? '', c.url ?? '');
139
- if (c.issues != null)
140
- out.push(JSON.stringify(candidateIssues(c)));
141
- const s = byKey.get(c.key);
142
- if (s == null)
143
- continue;
144
- out.push(JSON.stringify(storyDetail(c.key, byKey)));
145
- }
146
- return out;
147
- }
148
- //# sourceMappingURL=story-detail.js.map