ensemble-review 1.0.35 → 1.0.37

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 (186) hide show
  1. package/dist/api/plugins/rate-limit.js +22 -0
  2. package/dist/api/routes/releases.js +181 -0
  3. package/dist/app.js +3 -0
  4. package/dist/coding/story-build/base-refs.js +10 -2
  5. package/dist/coding/story-build/loop/driver.js +1 -1
  6. package/dist/coding/story-build/loop/routes.js +9 -1
  7. package/dist/coding/story-build/persist.js +53 -7
  8. package/dist/coding/story-build/plan-persist.js +1 -1
  9. package/dist/coding/story-build/push-hotfix.js +293 -0
  10. package/dist/coding/story-build/push.js +5 -0
  11. package/dist/coding/story-build/routes-build.js +25 -3
  12. package/dist/db/erase-account.js +14 -0
  13. package/dist/db/hotfix-bases.js +31 -0
  14. package/dist/db/migrations/0195_releases.sql +128 -0
  15. package/dist/db/migrations/0196_story_build_hotfix.sql +54 -0
  16. package/dist/db/migrations/0197_release_items_live_tag.sql +11 -0
  17. package/dist/db/migrations/0198_story_build_port_report.sql +7 -0
  18. package/dist/db/migrations/meta/_journal.json +28 -0
  19. package/dist/db/queries.js +21 -0
  20. package/dist/db/release-check.js +378 -0
  21. package/dist/db/release-settings.js +94 -0
  22. package/dist/db/repo-tags.js +82 -0
  23. package/dist/db/schema.sqlite.js +246 -0
  24. package/dist/github/mutations.js +67 -0
  25. package/dist/github/queries.js +5 -0
  26. package/dist/github/settle-query.js +11 -1
  27. package/dist/github/tag-queries.js +101 -0
  28. package/dist/releases/create.js +539 -0
  29. package/dist/releases/github.js +78 -0
  30. package/dist/releases/notes.js +152 -0
  31. package/dist/releases/refresh.js +43 -0
  32. package/dist/sandbox/session/manager.js +207 -12
  33. package/dist/sandbox/session/plan.js +19 -7
  34. package/dist/sandbox/session/routes.js +12 -2
  35. package/dist/sandbox/workspace/hotfix.js +30 -0
  36. package/dist/sandbox/workspace/index.js +38 -2
  37. package/dist/sandbox/workspace/port-report.js +137 -0
  38. package/dist/sandbox/workspace/seed.js +40 -0
  39. package/dist/shared/hotfix.js +165 -0
  40. package/dist/shared/index.js +4 -0
  41. package/dist/shared/release-check.js +528 -0
  42. package/dist/shared/release-write.js +264 -0
  43. package/dist/shared/releases.js +89 -0
  44. package/dist/shared/story-build.js +2 -0
  45. package/dist/sync/repo-tags.js +368 -0
  46. package/dist/sync/settle-sweep.js +47 -2
  47. package/dist/sync/sync-repo.js +12 -0
  48. package/dist/sync/tag-ranges.js +350 -0
  49. package/dist/sync/tags-step.js +77 -0
  50. package/dist/sync/upsert.js +23 -0
  51. package/package.json +5 -2
  52. package/public/assets/{AiFixTab-CVIZl9I8.js → AiFixTab-BC3tDZ3z.js} +1 -1
  53. package/public/assets/{PrBotBehaviourTab-q0M7RU1e.js → PrBotBehaviourTab-C5j5uhLn.js} +1 -1
  54. package/public/assets/index-BrjUDY3j.js +138 -0
  55. package/public/assets/index-D1zhsqSF.css +1 -0
  56. package/public/index.html +2 -2
  57. package/dist/ai/check-tool.js +0 -6
  58. package/dist/api/routes/auth.js +0 -194
  59. package/dist/api/routes/billing.js +0 -209
  60. package/dist/api/routes/claude-review.js +0 -522
  61. package/dist/api/routes/contact.js +0 -318
  62. package/dist/api/routes/feed.js +0 -21
  63. package/dist/api/routes/inbox.js +0 -45
  64. package/dist/api/routes/mentions.js +0 -22
  65. package/dist/api/routes/teams.js +0 -150
  66. package/dist/api/routes/webhooks.js +0 -181
  67. package/dist/coding/activity.js +0 -80
  68. package/dist/coding/agent.js +0 -147
  69. package/dist/coding/ai-fix/checks-preview.js +0 -58
  70. package/dist/coding/ai-fix/comment-seed.js +0 -519
  71. package/dist/coding/ai-fix/prompts.js +0 -283
  72. package/dist/coding/ai-resolve/prompt.js +0 -161
  73. package/dist/coding/ai-resolve/routes.js +0 -97
  74. package/dist/coding/ai-resolve/run.js +0 -343
  75. package/dist/coding/ai-resolve/schema.js +0 -26
  76. package/dist/coding/ai-resolve/validate.js +0 -136
  77. package/dist/coding/checks/deny-scan.js +0 -75
  78. package/dist/coding/checks/discover.js +0 -541
  79. package/dist/coding/checks/docker.js +0 -38
  80. package/dist/coding/checks/env.js +0 -88
  81. package/dist/coding/checks/hooks.js +0 -91
  82. package/dist/coding/checks/index.js +0 -8
  83. package/dist/coding/checks/local.js +0 -40
  84. package/dist/coding/checks/sandbox.js +0 -206
  85. package/dist/coding/checks/session.js +0 -518
  86. package/dist/coding/checks/spawn.js +0 -123
  87. package/dist/coding/checks/spec.js +0 -140
  88. package/dist/coding/checks/types.js +0 -6
  89. package/dist/coding/partial-spend.js +0 -19
  90. package/dist/coding/safe-path.js +0 -75
  91. package/dist/coding/story-build/build-prompts.js +0 -178
  92. package/dist/coding/story-build/build.js +0 -332
  93. package/dist/coding/story-build/checkouts.js +0 -66
  94. package/dist/coding/story-build/docs-runner.js +0 -178
  95. package/dist/coding/story-build/docs.js +0 -334
  96. package/dist/coding/story-build/manager.js +0 -999
  97. package/dist/coding/story-build/plan-agent.js +0 -190
  98. package/dist/coding/story-build/plan.js +0 -338
  99. package/dist/coding/story-build/prompts.js +0 -147
  100. package/dist/coding/story-build/routes-docs.js +0 -113
  101. package/dist/coding/story-build/routes.js +0 -295
  102. package/dist/coding/story-build/schema.js +0 -53
  103. package/dist/db/automation-output.js +0 -349
  104. package/dist/db/credits.js +0 -79
  105. package/dist/db/schema.js +0 -321
  106. package/dist/db/schema.pg.js +0 -2197
  107. package/dist/db/sprint-repo-suggest.js +0 -257
  108. package/dist/db/sprint-stories.js +0 -42
  109. package/dist/db/sprint-story.js +0 -505
  110. package/dist/db/sprint-ticket-repos.js +0 -195
  111. package/dist/db/story-selection.js +0 -270
  112. package/dist/db/team-comparison.js +0 -124
  113. package/dist/db/workspace-comparison.js +0 -98
  114. package/dist/feed/fyi-provider.js +0 -10
  115. package/dist/github/local-user.js +0 -92
  116. package/dist/github/repo-files.js +0 -26
  117. package/dist/ml/worker.js +0 -31
  118. package/dist/platform/advisor/discovery/tier2.js +0 -88
  119. package/dist/platform/advisor/discovery/tier3.js +0 -57
  120. package/dist/platform/advisor/emitters/adapter.js +0 -5
  121. package/dist/platform/advisor/emitters/coderabbit.js +0 -175
  122. package/dist/platform/advisor/emitters/copilot-prose.js +0 -118
  123. package/dist/platform/advisor/emitters/greptile.js +0 -176
  124. package/dist/platform/advisor/emitters/lint.js +0 -37
  125. package/dist/platform/advisor/emitters/manifest.js +0 -239
  126. package/dist/platform/advisor/emitters/prose.js +0 -183
  127. package/dist/platform/advisor/emitters/registry.js +0 -20
  128. package/dist/platform/advisor/emitters/yaml-doc.js +0 -95
  129. package/dist/platform/advisor/evidence.js +0 -79
  130. package/dist/platform/advisor/findings.js +0 -15
  131. package/dist/platform/advisor/intents.js +0 -269
  132. package/dist/platform/advisor/measure/effect.js +0 -18
  133. package/dist/platform/advisor/outputs/brief.js +0 -68
  134. package/dist/platform/advisor/outputs/config-pr.js +0 -156
  135. package/dist/platform/advisor/refine/diff-guard.js +0 -38
  136. package/dist/platform/advisor/refine/refine.js +0 -35
  137. package/dist/platform/advisor/retro-check.js +0 -98
  138. package/dist/platform/advisor/routes.js +0 -648
  139. package/dist/platform/advisor/store.js +0 -321
  140. package/dist/platform/advisor/taxonomy.js +0 -67
  141. package/dist/platform/annotations/prompts.js +0 -84
  142. package/dist/platform/annotations/routes.js +0 -86
  143. package/dist/platform/annotations/runner.js +0 -234
  144. package/dist/platform/annotations/targets.js +0 -203
  145. package/dist/platform/insights/grounded-figures.js +0 -162
  146. package/dist/platform/insights/period-questions.js +0 -235
  147. package/dist/platform/work-plan/hash.js +0 -99
  148. package/dist/platform/work-plan/prompt.js +0 -209
  149. package/dist/platform/work-plan/routes.js +0 -350
  150. package/dist/pro/bind.js +0 -309
  151. package/dist/pro/contract.js +0 -36
  152. package/dist/pro/event-hooks.js +0 -37
  153. package/dist/pro/migrate.js +0 -64
  154. package/dist/review/events.js +0 -24
  155. package/dist/review/local-settings.js +0 -78
  156. package/dist/review/memory/capture.js +0 -0
  157. package/dist/review/memory/retrieval.js +0 -272
  158. package/dist/review/memory/routes.js +0 -78
  159. package/dist/review/persist.js +0 -203
  160. package/dist/review/plugin-providers.js +0 -12
  161. package/dist/review/prompt.js +0 -264
  162. package/dist/review/review-manager.js +0 -259
  163. package/dist/review/routing.js +0 -164
  164. package/dist/review/sprint-assess/agent.js +0 -145
  165. package/dist/review/sprint-assess/manager.js +0 -382
  166. package/dist/review/sprint-assess/prompts.js +0 -99
  167. package/dist/review/sprint-assess/routes.js +0 -109
  168. package/dist/review/sprint-assess/schema.js +0 -70
  169. package/dist/review/sprint-assess/validate.js +0 -180
  170. package/dist/review/sprint-story/manager.js +0 -98
  171. package/dist/review/sprint-story/prompt.js +0 -45
  172. package/dist/review/sprint-story/validate.js +0 -127
  173. package/dist/review/story-docs/build-prompts.js +0 -153
  174. package/dist/review/story-docs/build-validate.js +0 -162
  175. package/dist/review/story-report/agent.js +0 -203
  176. package/dist/review/story-report/fingerprint.js +0 -45
  177. package/dist/review/story-report/manager.js +0 -287
  178. package/dist/review/story-report/routes.js +0 -154
  179. package/dist/review/story-report/story-detail.js +0 -148
  180. package/dist/review/story-report/validate.js +0 -105
  181. package/dist/review/ticket-review/sweep-waits.js +0 -20
  182. package/dist/review/write-deny.js +0 -170
  183. package/dist/shared/conflict-ai.js +0 -24
  184. package/dist/sync/benchmark-rollup.js +0 -50
  185. package/public/assets/index-CbEfmqOD.css +0 -1
  186. package/public/assets/index-DBK4RWQu.js +0 -136
@@ -0,0 +1,293 @@
1
+ import { hotfixBranchState, hotfixPairLine, hotfixPortBodyLine, hotfixPushTargets, } from '../../shared/index.js';
2
+ import { sessionBranch } from '../../sandbox/workspace/seed.js';
3
+ import { dropSessionBaseRefs } from './base-refs.js';
4
+ import { getStoryBuildRepoRow, getStoryBuildRow, listStoryBuildRepoRows, storyBuildRepoNames, storyBuildRepoPort, updateStoryBuildRepo, } from './persist.js';
5
+ import { closesFor, coveredBy, defangGithubText, linkTicket, repoFacts, storyBuildPrBody, storyBuildPrTitle } from './push.js';
6
+ // PUSH ONE REPOSITORY OF A HOTFIX BUILD (docs/STORY-BUILD.md § Hotfix builds; docs/RELEASES.md § Hotfix). The
7
+ // reader's press. Every write is the READER'S (origin 'reader': no ledger, no trailer), exactly as Push:
8
+ //
9
+ // 1. `hotfix/<latest release tag>` — read live; made at the release commit when missing (a NEW ref, never
10
+ // moved). When it has MOVED past the release commit, PR 1 still targets it, and the open PRs already on
11
+ // it are recorded for the tab.
12
+ // 2. PR 1 `<KEY>` → `hotfix/<tag>`: the RELEASE FIX (the row's patch, on the release commit).
13
+ // 3. PR 2 `<KEY>-main` → the default branch: the MAIN FIX (`main_patch`, on the default branch's base).
14
+ // 4. PR 1's body edited ONCE to link PR 2 (PR 2's body links PR 1 from the start).
15
+ //
16
+ // Both titled `<KEY>: <title>`, draft by default (the toggle), `BRANCH_EXISTS` refused, push permission
17
+ // re-checked. A second press pushes only what is left; once both PRs are open nothing more is written.
18
+ // PR 2 opens only when the main fix changed something: a port report of `not_needed` with an unchanged main
19
+ // checkout opens PR 1 alone. Both bodies carry the port report's one line (shared `hotfixPortBodyLine`) —
20
+ // never when the patches contradict the claim (the tab shows that flag; Push stays the reader's call).
21
+ const pushing = new Set();
22
+ /** A hotfix push of this repository row is in flight. */
23
+ export function isHotfixRepoPushing(repoRowId) {
24
+ return pushing.has(repoRowId);
25
+ }
26
+ export const defaultHotfixPushDeps = {
27
+ async canPush(ctx, accountId, repoId) {
28
+ const { getViewerCanPush } = await import('../ai-fix/pr-context.js');
29
+ return getViewerCanPush(ctx, accountId, repoId);
30
+ },
31
+ async token(accountId) {
32
+ const { getAccessToken } = await import('../../auth/account.js');
33
+ return getAccessToken(accountId);
34
+ },
35
+ async branchSha(token, owner, name, branch) {
36
+ const { fetchBranchHeadSha } = await import('../../github/mutations.js');
37
+ return fetchBranchHeadSha(token, owner, name, branch);
38
+ },
39
+ async createBranch(token, a) {
40
+ const { createHotfixBranch } = await import('../../github/mutations.js');
41
+ return createHotfixBranch(token, a, 'reader');
42
+ },
43
+ async openPrsOnBase(token, owner, name, branch) {
44
+ const { fetchOpenPrsOnBase } = await import('../../github/mutations.js');
45
+ return fetchOpenPrsOnBase(token, owner, name, branch);
46
+ },
47
+ async pushAndOpen(a) {
48
+ const { applyAndPushBranch } = await import('../git-ops.js');
49
+ return applyAndPushBranch(a);
50
+ },
51
+ async updateBody(token, owner, name, number, body) {
52
+ const { updatePullRequestBody } = await import('../../github/mutations.js');
53
+ const r = await updatePullRequestBody(token, owner, name, number, body, 'reader');
54
+ return r.ok ? { ok: true } : { ok: false, message: r.message };
55
+ },
56
+ now: () => new Date(),
57
+ };
58
+ let deps = defaultHotfixPushDeps;
59
+ /** Test seam (null restores the real steps). */
60
+ export function setHotfixPushDepsForTests(d) {
61
+ deps = d == null ? defaultHotfixPushDeps : { ...defaultHotfixPushDeps, ...d };
62
+ }
63
+ const result = (repoId, outcome, code, error, pr = null) => ({ repoId, outcome, prNumber: pr?.number ?? null, prUrl: pr?.url ?? null, code, error });
64
+ /** The body of one of the pair (pure apart from the shared builders): the usual body, then the pair line. */
65
+ export function hotfixPrBody(build, repo, o) {
66
+ const base = storyBuildPrBody({
67
+ summary: repo.summary ?? null,
68
+ ticketKey: build.ticketKey,
69
+ ticketTitle: build.ticketTitle ?? build.seedJson?.story.title ?? null,
70
+ ticketUrl: build.seedJson?.story.url ?? null,
71
+ covers: coveredBy(build, repo),
72
+ siblings: [],
73
+ stackedOnPrNumber: null,
74
+ // A hotfix is closed by the default-branch PR only (the release PR merges into a side branch).
75
+ closes: o.self === 'main' ? closesFor(build, o.includedRepos) : null,
76
+ });
77
+ const where = o.self === 'release' ? `**Release:** this fixes ${o.tagName}.` : `**Release:** this ports the fix for ${o.tagName} to ${o.defaultBranch}.`;
78
+ // THE PORT REPORT's one line (the agent's claim; null when there is none or the patches contradict it).
79
+ // Untrusted model text: defanged like every other text sent to GitHub.
80
+ const port = storyBuildRepoPort(repo);
81
+ const portLine = hotfixPortBodyLine(port);
82
+ const adapted = port.flag == null && port.claim?.outcome === 'adapted';
83
+ return [
84
+ base,
85
+ '',
86
+ where,
87
+ ...(portLine != null ? [defangGithubText(portLine)] : []),
88
+ ...(o.paired ? [hotfixPairLine(o.self, { tagName: o.tagName, defaultBranch: o.defaultBranch, other: o.other, adapted })] : []),
89
+ ].join('\n');
90
+ }
91
+ export async function pushHotfixRepo(ctx, a) {
92
+ if (a.branch !== undefined)
93
+ return result(a.repoId, 'failed', 'PUSH_DENIED', 'A hotfix build pushes the ticket-key branches. Discard it and start again to use another name.');
94
+ const build = await getStoryBuildRow(ctx, a.accountId, a.buildId);
95
+ if (!build)
96
+ return result(a.repoId, 'failed', 'NotFound', 'Build not found.');
97
+ const repo = await getStoryBuildRepoRow(ctx, a.accountId, a.buildId, a.repoId);
98
+ if (!repo)
99
+ return result(a.repoId, 'failed', 'NotFound', 'This repository is not part of the build.');
100
+ if (repo.status === 'pushed') {
101
+ const pr = repo.pushedPrNumber != null ? { number: repo.pushedPrNumber, url: repo.pushedPrUrl ?? '' } : null;
102
+ return result(a.repoId, 'skipped', 'ALREADY_PUSHED', null, pr);
103
+ }
104
+ if (repo.readOnly)
105
+ return result(a.repoId, 'skipped', 'READ_ONLY', 'This repository has no release yet, so it is read only in this hotfix build.');
106
+ if (!repo.include)
107
+ return result(a.repoId, 'skipped', 'NOT_INCLUDED', null);
108
+ if (build.status !== 'ready') {
109
+ const why = build.status === 'building' ? 'Wait for the build to finish.' : 'This build has nothing to push.';
110
+ return result(a.repoId, 'failed', build.status === 'building' ? 'BUSY' : 'NOT_READY', why);
111
+ }
112
+ if (repo.status !== 'ready' || repo.hotfixTag == null)
113
+ return result(a.repoId, 'failed', 'NOT_READY', 'This repository has nothing to push.');
114
+ if (pushing.has(repo.id))
115
+ return result(a.repoId, 'skipped', 'PUSH_IN_PROGRESS', 'This repository is being pushed.');
116
+ pushing.add(repo.id);
117
+ try {
118
+ return await pushClaimed(ctx, a, build, repo);
119
+ }
120
+ finally {
121
+ pushing.delete(repo.id);
122
+ }
123
+ }
124
+ async function pushClaimed(ctx, a, build, repo) {
125
+ const tagName = repo.hotfixTag;
126
+ const releaseSha = repo.baseSha;
127
+ const fail = async (code, message, main = false) => {
128
+ await updateStoryBuildRepo(ctx, a.accountId, repo.id, main ? { mainPushError: message, mainPushErrorCode: code } : { pushError: message, pushErrorCode: code }).catch(() => { });
129
+ return result(a.repoId, 'failed', code, message);
130
+ };
131
+ if (!(await deps.canPush(ctx, a.accountId, repo.repoId)))
132
+ return fail('NO_PUSH_PERMISSION', 'You cannot push to this repository.');
133
+ const names = await storyBuildRepoNames(ctx, a.accountId, [repo.repoId]);
134
+ const fullName = names.get(repo.repoId) ?? null;
135
+ const [owner, name] = (fullName ?? '').split('/');
136
+ if (!fullName || !owner || !name)
137
+ return fail('NotFound', 'This repository is not known here any more.');
138
+ const facts = await repoFacts(ctx, a.accountId, repo.repoId);
139
+ const defaultBranch = facts?.defaultBranch ?? (repo.mainBaseRef && repo.mainBaseRef !== 'HEAD' ? repo.mainBaseRef : null);
140
+ if (!defaultBranch)
141
+ return fail('NOT_READY', 'The default branch of this repository is not known yet. Try again after the next sync.');
142
+ const branch = (repo.branch ?? '').trim() || sessionBranch(build.ticketKey);
143
+ const targets = hotfixPushTargets({ branch, tagName, defaultBranch });
144
+ const protect = [...new Set([defaultBranch, targets.release.base, tagName])];
145
+ const title = storyBuildPrTitle(build.ticketKey, build.ticketTitle ?? build.seedJson?.story.title ?? null, repo.commitMessage ?? repo.mainCommitMessage ?? null);
146
+ const included = (await listStoryBuildRepoRows(ctx, a.accountId, build.id)).filter((r) => r.include && !r.readOnly).length;
147
+ const token = await deps.token(a.accountId);
148
+ const afterSync = (prNumber) => linkTicket(ctx, a.accountId, repo.repoId, prNumber, build.ident);
149
+ let releasePr = repo.pushedPrNumber != null ? { number: repo.pushedPrNumber, url: repo.pushedPrUrl ?? null } : null;
150
+ let mainPr = repo.mainPushedPrNumber != null ? { number: repo.mainPushedPrNumber, url: repo.mainPushedPrUrl ?? null } : null;
151
+ // Both already open at the press: PR 1's body was linked then (or that one edit failed: never retried).
152
+ let releaseBodyLinked = releasePr != null && mainPr != null;
153
+ const releaseWanted = repo.patch != null && repo.patch !== '';
154
+ const mainWanted = (repo.mainStatus === 'ready' || repo.mainStatus === 'pushed') && repo.mainPatch != null && repo.mainPatch !== '';
155
+ const paired = releaseWanted && mainWanted;
156
+ // ── 1 + 2: the hotfix branch, then PR 1 (the release fix). ──
157
+ if (releaseWanted && releasePr == null) {
158
+ let live;
159
+ try {
160
+ live = await deps.branchSha(token, owner, name, targets.release.base);
161
+ }
162
+ catch {
163
+ live = null;
164
+ }
165
+ let created = false;
166
+ if (hotfixBranchState(live, releaseSha) === 'missing') {
167
+ try {
168
+ const r = await deps.createBranch(token, { owner, name, branch: targets.release.base, sha: releaseSha });
169
+ created = r.outcome === 'created';
170
+ live = created ? releaseSha : await deps.branchSha(token, owner, name, targets.release.base).catch(() => null);
171
+ }
172
+ catch (err) {
173
+ return fail('BRANCH_NOT_CREATED', `GitHub did not make ${targets.release.base} on ${fullName}: ${err instanceof Error ? err.message : String(err)}`);
174
+ }
175
+ if (live == null)
176
+ return fail('BRANCH_NOT_CREATED', `${targets.release.base} could not be read on ${fullName}. Try again.`);
177
+ }
178
+ const moved = hotfixBranchState(live, releaseSha) === 'moved';
179
+ // The branch was already there (an earlier hotfix off this release): name the PRs open on it,
180
+ // whether or not it has moved — an open, unmerged PR 1 leaves it at the release commit.
181
+ const prs = !created ? await deps.openPrsOnBase(token, owner, name, targets.release.base).catch(() => []) : [];
182
+ const record = { name: targets.release.base, releaseSha, created, movedToSha: moved ? live : null, prs, at: deps.now().toISOString() };
183
+ await updateStoryBuildRepo(ctx, a.accountId, repo.id, { hotfixBranchJson: record });
184
+ const body = hotfixPrBody(build, repo, { self: 'release', tagName, defaultBranch, other: mainPr, paired, includedRepos: included });
185
+ try {
186
+ const pushed = await deps.pushAndOpen({
187
+ accountId: a.accountId,
188
+ owner,
189
+ name,
190
+ baseSha: releaseSha,
191
+ baseRef: targets.release.base,
192
+ patch: repo.patch,
193
+ commitMessage: defangGithubText((repo.commitMessage ?? '').trim() || title, { mentions: false }),
194
+ branch: targets.release.head,
195
+ prBase: targets.release.base,
196
+ title,
197
+ body,
198
+ draft: a.draft,
199
+ protect: [...protect, targets.main.head],
200
+ afterSync,
201
+ });
202
+ releasePr = { number: pushed.prNumber, url: pushed.prUrl };
203
+ releaseBodyLinked = mainPr != null;
204
+ await updateStoryBuildRepo(ctx, a.accountId, repo.id, {
205
+ branch,
206
+ pushedBranch: pushed.pushedBranch,
207
+ pushedPrNumber: pushed.prNumber,
208
+ pushedPrUrl: pushed.prUrl,
209
+ pushedCommitSha: pushed.commitSha,
210
+ pushedAt: deps.now(),
211
+ prTitle: title,
212
+ prBody: body,
213
+ pushError: null,
214
+ pushErrorCode: null,
215
+ });
216
+ }
217
+ catch (err) {
218
+ return pushFailed(ctx, a, repo, err, targets.release.head, fullName, false);
219
+ }
220
+ }
221
+ // ── 3: PR 2 (the main fix). ──
222
+ if (mainWanted && mainPr == null) {
223
+ const body = hotfixPrBody(build, repo, { self: 'main', tagName, defaultBranch, other: releasePr, paired, includedRepos: included });
224
+ try {
225
+ const pushed = await deps.pushAndOpen({
226
+ accountId: a.accountId,
227
+ owner,
228
+ name,
229
+ baseSha: repo.mainBaseSha ?? '',
230
+ baseRef: defaultBranch,
231
+ patch: repo.mainPatch,
232
+ commitMessage: defangGithubText((repo.mainCommitMessage ?? repo.commitMessage ?? '').trim() || title, { mentions: false }),
233
+ branch: targets.main.head,
234
+ prBase: targets.main.base,
235
+ title,
236
+ body,
237
+ draft: a.draft,
238
+ protect: [...protect, targets.release.head],
239
+ afterSync,
240
+ });
241
+ mainPr = { number: pushed.prNumber, url: pushed.prUrl };
242
+ await updateStoryBuildRepo(ctx, a.accountId, repo.id, {
243
+ mainStatus: 'pushed',
244
+ mainPushedBranch: pushed.pushedBranch,
245
+ mainPushedPrNumber: pushed.prNumber,
246
+ mainPushedPrUrl: pushed.prUrl,
247
+ mainPushedCommitSha: pushed.commitSha,
248
+ mainPushedAt: deps.now(),
249
+ mainPushError: null,
250
+ mainPushErrorCode: null,
251
+ });
252
+ }
253
+ catch (err) {
254
+ return pushFailed(ctx, a, repo, err, targets.main.head, fullName, true);
255
+ }
256
+ }
257
+ // ── 4: PR 1's body links PR 2, once. A failure is a line, never retried. ──
258
+ if (releasePr != null && mainPr != null && !releaseBodyLinked) {
259
+ const body = hotfixPrBody(build, repo, { self: 'release', tagName, defaultBranch, other: mainPr, paired, includedRepos: included });
260
+ const r = await deps.updateBody(token, owner, name, releasePr.number, body).catch((err) => ({ ok: false, message: err instanceof Error ? err.message : String(err) }));
261
+ if (r.ok)
262
+ await updateStoryBuildRepo(ctx, a.accountId, repo.id, { prBody: body }).catch(() => { });
263
+ else
264
+ ctx.log.warn(`story build ${build.id}: linking the hotfix pair failed: ${r.message ?? ''}`);
265
+ }
266
+ const done = (!releaseWanted || releasePr != null) && (!mainWanted || mainPr != null);
267
+ if (done) {
268
+ await updateStoryBuildRepo(ctx, a.accountId, repo.id, { status: 'pushed' }).catch(() => { });
269
+ // Nothing left to push: the session's pinned base commits (release AND main) are needed no more.
270
+ const after = await listStoryBuildRepoRows(ctx, a.accountId, build.id).catch(() => null);
271
+ if (after != null && after.every((r) => !r.include || r.status !== 'ready')) {
272
+ const all = await storyBuildRepoNames(ctx, a.accountId, after.map((r) => r.repoId)).catch(() => new Map());
273
+ void dropSessionBaseRefs([...all.values(), fullName], build.id);
274
+ }
275
+ }
276
+ const shown = releasePr ?? mainPr;
277
+ return result(a.repoId, 'pushed', null, null, shown ? { number: shown.number, url: shown.url ?? '' } : null);
278
+ }
279
+ async function pushFailed(ctx, a, repo, err, branch, fullName, main) {
280
+ const code = err?.code ?? 'failed';
281
+ const message = code === 'BRANCH_EXISTS'
282
+ ? `A branch named ${branch} already exists on ${fullName}. Delete it on GitHub or discard this build.`
283
+ : err instanceof Error
284
+ ? err.message
285
+ : String(err);
286
+ if (code !== 'BRANCH_EXISTS' && code !== 'PR_NOT_OPENED' && code !== 'PUSH_DENIED' && code !== 'APPLY_FAILED')
287
+ ctx.log.warn({ err }, 'story build hotfix push failed');
288
+ await updateStoryBuildRepo(ctx, a.accountId, repo.id, main
289
+ ? { mainPushError: message, mainPushErrorCode: code, ...(code === 'PR_NOT_OPENED' ? { mainPushedBranch: branch, mainPushedCommitSha: err.commitSha ?? null } : {}) }
290
+ : { pushError: message, pushErrorCode: code, ...(code === 'PR_NOT_OPENED' ? { pushedBranch: branch, pushedCommitSha: err.commitSha ?? null } : {}) }).catch(() => { });
291
+ return result(a.repoId, 'failed', code, message);
292
+ }
293
+ //# sourceMappingURL=push-hotfix.js.map
@@ -242,6 +242,11 @@ export const pushRepo = async (ctx, a) => {
242
242
  const build = await getStoryBuildRow(ctx, a.accountId, a.buildId);
243
243
  if (!build)
244
244
  return result(a.repoId, 'failed', 'NotFound', 'Build not found.');
245
+ // A HOTFIX build pushes two PRs per repository (push-hotfix.ts; docs/STORY-BUILD.md § Hotfix builds).
246
+ if (build.hotfix === true) {
247
+ const { pushHotfixRepo } = await import('./push-hotfix.js');
248
+ return pushHotfixRepo(ctx, a);
249
+ }
245
250
  const repo = await getStoryBuildRepoRow(ctx, a.accountId, a.buildId, a.repoId);
246
251
  if (!repo)
247
252
  return result(a.repoId, 'failed', 'NotFound', 'This repository is not part of the build.');
@@ -1,4 +1,4 @@
1
- import { STORY_BUILD_API, STORY_BUILD_BRANCH_MAX, STORY_BUILD_PUSH_DRAFT_DEFAULT, storyBuildActivityOf, } from '../../shared/index.js';
1
+ import { HOTFIX_NO_LOOP_LINE, STORY_BUILD_API, STORY_BUILD_BRANCH_MAX, STORY_BUILD_PUSH_DRAFT_DEFAULT, storyBuildActivityOf, } from '../../shared/index.js';
2
2
  import { resolveWorkspaceScope } from '../../db/queries.js';
3
3
  import { getStoryBuildRepoPatch, getStoryBuildRepoRow, getStoryBuildRow, listActiveStoryBuildRows, updateStoryBuild } from './persist.js';
4
4
  import { getStoryBuildPrRow, latestPlanRow, toPlanRevision } from './plan-persist.js';
@@ -116,10 +116,15 @@ export function registerStoryBuildBuildRoutes(app, ctx, push = pushRepo, pushPr
116
116
  const repoId = parseId(req.params.repoId);
117
117
  if (id == null || repoId == null)
118
118
  return reply.code(404).send(NOT_FOUND);
119
- const got = await getStoryBuildRepoPatch(ctx, accountId, id, repoId);
119
+ // `?target=main`: a HOTFIX build's MAIN FIX (404 on a row without a main checkout).
120
+ const raw = req.query?.target;
121
+ if (raw !== undefined && raw !== 'release' && raw !== 'main')
122
+ return reply.code(400).send({ error: 'BadRequest', message: '`target` must be release or main.' });
123
+ const target = raw === 'main' ? 'main' : 'release';
124
+ const got = await getStoryBuildRepoPatch(ctx, accountId, id, repoId, target);
120
125
  if (!got)
121
126
  return reply.code(404).send(NOT_FOUND);
122
- const resp = { repoId, baseSha: got.baseSha, patch: got.patch, filesChanged: got.filesChanged };
127
+ const resp = { repoId, baseSha: got.baseSha, patch: got.patch, filesChanged: got.filesChanged, target };
123
128
  return reply.send(resp);
124
129
  });
125
130
  app.post(STORY_BUILD_API.repoPush, async (req, reply) => {
@@ -184,6 +189,13 @@ export function registerStoryBuildBuildRoutes(app, ctx, push = pushRepo, pushPr
184
189
  const on = b != null && typeof b === 'object' && !Array.isArray(b) ? b.on : undefined;
185
190
  if (typeof on !== 'boolean')
186
191
  return reply.code(400).send({ error: 'BadRequest', message: '`on` must be true or false.' });
192
+ if (on) {
193
+ const row = await getStoryBuildRow(ctx, accountId, id);
194
+ if (row == null)
195
+ return reply.code(404).send(NOT_FOUND);
196
+ if (row.hotfix === true)
197
+ return reply.code(409).send({ error: 'HotfixNoLoop', message: HOTFIX_NO_LOOP_LINE });
198
+ }
187
199
  const r = await setStoryBuildAutoPush(ctx, accountId, id, on);
188
200
  if (r.status === 'not_found')
189
201
  return reply.code(404).send(NOT_FOUND);
@@ -229,6 +241,16 @@ export function registerStoryBuildBuildRoutes(app, ctx, push = pushRepo, pushPr
229
241
  const resp = { ticketReviewDoneAt: at?.toISOString() ?? null };
230
242
  return reply.send(resp);
231
243
  });
244
+ // HOTFIX (docs/STORY-BUILD.md § Hotfix builds): the Start panel's base list — each workspace repository's
245
+ // latest release, or read only — and whether "Treat as a hotfix" is offered. DB only (the synced tags).
246
+ app.get(STORY_BUILD_API.hotfixBases, async (req, reply) => {
247
+ const accountId = ctx.accountIdOf(req);
248
+ // The session's own scope: EVERY member, external repos included (sandbox/session/routes.ts's start).
249
+ const scope = await resolveWorkspaceScope(accountId, req.query.workspace ?? null, null, { members: 'all' });
250
+ const { sessionHotfixBases } = await import('../../sandbox/session/manager.js');
251
+ const resp = await sessionHotfixBases(accountId, scope.workspaceId, scope.repoIds);
252
+ return reply.send(resp);
253
+ });
232
254
  // THE BUILDS TOAST (shared story-build-activity.ts): one row per build that runs or waits at a gate. DB
233
255
  // plus the in-process live map; never a tracker, GitHub or model call.
234
256
  app.get(STORY_BUILD_API.active, async (req, reply) => {
@@ -95,6 +95,14 @@ export async function eraseAccountData(accountId) {
95
95
  await tx.delete(workspaceRepos).where(eq(workspaceRepos.accountId, accountId)).execute();
96
96
  // The EXTRA External memberships (migration 0191) — same shape, same reason.
97
97
  await tx.delete(workspaceExternalRepos).where(eq(workspaceExternalRepos.accountId, accountId)).execute();
98
+ // RELEASES (migration 0195): child before parent (both children's composite FKs point at
99
+ // `releases`, which points at `workspaces`), then the synced tags and their ranges. The repo and
100
+ // workspace cascades normally take them; explicit so the guarantee depends on neither.
101
+ await tx.delete(schema.releaseTicketComments).where(eq(schema.releaseTicketComments.accountId, accountId)).execute();
102
+ await tx.delete(schema.releaseItems).where(eq(schema.releaseItems.accountId, accountId)).execute();
103
+ await tx.delete(schema.releases).where(eq(schema.releases.accountId, accountId)).execute();
104
+ await tx.delete(schema.repoTagRanges).where(eq(schema.repoTagRanges.accountId, accountId)).execute();
105
+ await tx.delete(schema.repoTags).where(eq(schema.repoTags.accountId, accountId)).execute();
98
106
  // Story chats (migration 0192): turns first (their FK names the chat), then the chats.
99
107
  await tx.delete(schema.storyChatTurns).where(eq(schema.storyChatTurns.accountId, accountId)).execute();
100
108
  await tx.delete(schema.storyChats).where(eq(schema.storyChats.accountId, accountId)).execute();
@@ -495,6 +503,12 @@ export function accountScopedTables() {
495
503
  { name: 'workspaceRepos', col: workspaceRepos.accountId, table: workspaceRepos },
496
504
  // The EXTRA External memberships (migration 0191): a repo External in more than one workspace.
497
505
  { name: 'workspaceExternalRepos', col: workspaceExternalRepos.accountId, table: workspaceExternalRepos },
506
+ // RELEASES (migration 0195): the synced tags, their ranges, and every release the reader made.
507
+ { name: 'repoTags', col: schema.repoTags.accountId, table: schema.repoTags },
508
+ { name: 'repoTagRanges', col: schema.repoTagRanges.accountId, table: schema.repoTagRanges },
509
+ { name: 'releases', col: schema.releases.accountId, table: schema.releases },
510
+ { name: 'releaseItems', col: schema.releaseItems.accountId, table: schema.releaseItems },
511
+ { name: 'releaseTicketComments', col: schema.releaseTicketComments.accountId, table: schema.releaseTicketComments },
498
512
  // Story chats (migration 0192): the reader's questions and the answers.
499
513
  { name: 'storyChats', col: schema.storyChats.accountId, table: schema.storyChats },
500
514
  { name: 'storyChatTurns', col: schema.storyChatTurns.accountId, table: schema.storyChatTurns },
@@ -0,0 +1,31 @@
1
+ // HOTFIX BASES (docs/STORY-BUILD.md § Hotfix builds, docs/RELEASES.md § Hotfix) — DB only, account-scoped.
2
+ // Per workspace repository its LATEST RELEASE (the newest release OR hotfix tag, shared `latestReleaseTag`)
3
+ // from the synced tags (`repo_tags`), through the ONE pure fold `hotfixBasesOf`. The Start panel's base list
4
+ // (`GET /api/story-builds/hotfix-bases`) and the session's bases (sandbox/session/manager.ts) both read it, so
5
+ // what the reader saw at Start is what the build starts from. Never a GitHub or tracker call.
6
+ import { and, eq, inArray } from 'drizzle-orm';
7
+ import { hotfixBasesOf } from '../shared/index.js';
8
+ import { db, schema } from './client.js';
9
+ import { getReleaseSettings } from './release-settings.js';
10
+ import { getRepoTags } from './repo-tags.js';
11
+ const { repos } = schema;
12
+ /** The hotfix bases of `repoIds` (the workspace's members, external included) under the workspace's suffixes. */
13
+ export async function getHotfixBases(accountId, workspaceId, repoIds) {
14
+ const settings = (await getReleaseSettings(accountId, workspaceId)) ?? { releaseSuffix: null, hotfixSuffix: null };
15
+ const rows = repoIds.length === 0
16
+ ? []
17
+ : await db
18
+ .select({ id: repos.id, owner: repos.owner, name: repos.name, defaultBranch: repos.defaultBranch, defaultBranchName: repos.defaultBranchName })
19
+ .from(repos)
20
+ .where(and(eq(repos.accountId, accountId), inArray(repos.id, [...repoIds])))
21
+ .execute();
22
+ const tags = await getRepoTags(accountId, rows.map((r) => r.id));
23
+ const folded = hotfixBasesOf(rows.map((r) => ({
24
+ repoId: r.id,
25
+ fullName: `${r.owner}/${r.name}`,
26
+ defaultBranch: r.defaultBranch ?? r.defaultBranchName ?? null,
27
+ tags: tags.filter((t) => t.repoId === r.id).map((t) => ({ name: t.name, sha: t.sha, committedAt: t.committedAt, taggedAt: t.taggedAt })),
28
+ })), settings);
29
+ return { workspaceId, ...folded };
30
+ }
31
+ //# sourceMappingURL=hotfix-bases.js.map
@@ -0,0 +1,128 @@
1
+ -- RELEASES (2026-11-14; docs/RELEASES.md). CORE, additive.
2
+ --
3
+ -- pull_requests.merge_commit_oid the commit a PR landed as (GraphQL mergeCommit.oid). Cleared only
4
+ -- on a positive statement (sync/upsert.ts).
5
+ -- workspaces.release_suffix / the per-workspace release and hotfix tag suffixes. NULL = not set;
6
+ -- workspaces.hotfix_suffix no default (shared validateReleaseSettings).
7
+ -- repos.tags_signature / tags_synced_at what the last complete tag read saw (sync/repo-tags.ts).
8
+ -- repo_tags NEW: one row per (account, repo, tag name).
9
+ -- repo_tag_ranges NEW: the PRs between a tag and the tag before it.
10
+ -- releases / release_items / release_ticket_comments NEW: one press of "Create release…", its
11
+ -- per-repo writes and its per-ticket tracker comments.
12
+ ALTER TABLE `pull_requests` ADD COLUMN `merge_commit_oid` text;
13
+ --> statement-breakpoint
14
+ CREATE INDEX IF NOT EXISTS `pr_account_repo_merge_commit_idx` ON `pull_requests` (`account_id`,`repo_id`,`merge_commit_oid`);
15
+ --> statement-breakpoint
16
+ ALTER TABLE `workspaces` ADD COLUMN `release_suffix` text;
17
+ --> statement-breakpoint
18
+ ALTER TABLE `workspaces` ADD COLUMN `hotfix_suffix` text;
19
+ --> statement-breakpoint
20
+ ALTER TABLE `repos` ADD COLUMN `tags_signature` text;
21
+ --> statement-breakpoint
22
+ ALTER TABLE `repos` ADD COLUMN `tags_synced_at` integer;
23
+ --> statement-breakpoint
24
+ CREATE TABLE IF NOT EXISTS `repo_tags` (
25
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
26
+ `account_id` integer NOT NULL,
27
+ `repo_id` integer NOT NULL,
28
+ `name` text NOT NULL,
29
+ `sha` text NOT NULL,
30
+ `tag_object_oid` text,
31
+ `committed_at` integer,
32
+ `tagged_at` integer,
33
+ `on_default_branch` integer,
34
+ `ancestry_head_sha` text,
35
+ `previous_sha` text,
36
+ `moved_at` integer,
37
+ `first_seen_at` integer NOT NULL,
38
+ `last_seen_at` integer NOT NULL,
39
+ FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
40
+ CONSTRAINT `repo_tags_repo_account_fk` FOREIGN KEY (`repo_id`,`account_id`) REFERENCES `repos`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
41
+ );
42
+ --> statement-breakpoint
43
+ CREATE UNIQUE INDEX IF NOT EXISTS `repo_tags_account_repo_name` ON `repo_tags` (`account_id`,`repo_id`,`name`);
44
+ --> statement-breakpoint
45
+ CREATE INDEX IF NOT EXISTS `repo_tags_account_repo_sha_idx` ON `repo_tags` (`account_id`,`repo_id`,`sha`);
46
+ --> statement-breakpoint
47
+ CREATE TABLE IF NOT EXISTS `repo_tag_ranges` (
48
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
49
+ `account_id` integer NOT NULL,
50
+ `repo_id` integer NOT NULL,
51
+ `tag_name` text NOT NULL,
52
+ `sha` text NOT NULL,
53
+ `base_tag_name` text,
54
+ `base_sha` text,
55
+ `method` text NOT NULL,
56
+ `pr_numbers` text NOT NULL,
57
+ `commit_count` integer DEFAULT 0 NOT NULL,
58
+ `unmatched_commits` integer DEFAULT 0 NOT NULL,
59
+ `truncated` integer DEFAULT 0 NOT NULL,
60
+ `moved_from_sha` text,
61
+ `computed_at` integer NOT NULL,
62
+ FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
63
+ CONSTRAINT `repo_tag_ranges_repo_account_fk` FOREIGN KEY (`repo_id`,`account_id`) REFERENCES `repos`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
64
+ );
65
+ --> statement-breakpoint
66
+ CREATE UNIQUE INDEX IF NOT EXISTS `repo_tag_ranges_account_repo_tag` ON `repo_tag_ranges` (`account_id`,`repo_id`,`tag_name`);
67
+ --> statement-breakpoint
68
+ CREATE TABLE IF NOT EXISTS `releases` (
69
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
70
+ `account_id` integer NOT NULL,
71
+ `workspace_id` integer NOT NULL,
72
+ `created_by_user_id` integer,
73
+ `created_by_login` text,
74
+ `created_at` integer NOT NULL,
75
+ `release_suffix` text NOT NULL,
76
+ `notes_markdown` text DEFAULT '' NOT NULL,
77
+ `state` text NOT NULL,
78
+ `finished_at` integer,
79
+ FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
80
+ FOREIGN KEY (`created_by_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
81
+ CONSTRAINT `releases_workspace_account_fk` FOREIGN KEY (`workspace_id`,`account_id`) REFERENCES `workspaces`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
82
+ );
83
+ --> statement-breakpoint
84
+ CREATE INDEX IF NOT EXISTS `releases_account_workspace_idx` ON `releases` (`account_id`,`workspace_id`,`created_at`);
85
+ --> statement-breakpoint
86
+ CREATE UNIQUE INDEX IF NOT EXISTS `releases_id_account` ON `releases` (`id`,`account_id`);
87
+ --> statement-breakpoint
88
+ CREATE TABLE IF NOT EXISTS `release_items` (
89
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
90
+ `account_id` integer NOT NULL,
91
+ `release_id` integer NOT NULL,
92
+ `repo_id` integer NOT NULL,
93
+ `version_tag` text NOT NULL,
94
+ `sha` text NOT NULL,
95
+ `release_tag_name` text NOT NULL,
96
+ `notes_markdown` text DEFAULT '' NOT NULL,
97
+ `tag_state` text DEFAULT 'pending' NOT NULL,
98
+ `tag_object_oid` text,
99
+ `prerelease_state` text DEFAULT 'pending' NOT NULL,
100
+ `github_release_id` integer,
101
+ `github_release_url` text,
102
+ `error` text,
103
+ `updated_at` integer NOT NULL,
104
+ FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
105
+ CONSTRAINT `release_items_release_account_fk` FOREIGN KEY (`release_id`,`account_id`) REFERENCES `releases`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade,
106
+ CONSTRAINT `release_items_repo_account_fk` FOREIGN KEY (`repo_id`,`account_id`) REFERENCES `repos`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
107
+ );
108
+ --> statement-breakpoint
109
+ CREATE UNIQUE INDEX IF NOT EXISTS `release_items_account_release_repo` ON `release_items` (`account_id`,`release_id`,`repo_id`);
110
+ --> statement-breakpoint
111
+ CREATE UNIQUE INDEX IF NOT EXISTS `release_items_account_repo_tag` ON `release_items` (`account_id`,`repo_id`,`release_tag_name`);
112
+ --> statement-breakpoint
113
+ CREATE TABLE IF NOT EXISTS `release_ticket_comments` (
114
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
115
+ `account_id` integer NOT NULL,
116
+ `release_id` integer NOT NULL,
117
+ `ticket_key` text NOT NULL,
118
+ `tracker_kind` text,
119
+ `state` text DEFAULT 'pending' NOT NULL,
120
+ `comment_id` text,
121
+ `error` text,
122
+ `claimed_at` integer,
123
+ `posted_at` integer,
124
+ FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
125
+ CONSTRAINT `release_ticket_comments_release_account_fk` FOREIGN KEY (`release_id`,`account_id`) REFERENCES `releases`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
126
+ );
127
+ --> statement-breakpoint
128
+ CREATE UNIQUE INDEX IF NOT EXISTS `release_ticket_comments_account_release_ticket` ON `release_ticket_comments` (`account_id`,`release_id`,`ticket_key`);
@@ -0,0 +1,54 @@
1
+ -- IMPLEMENT STORY: HOTFIX BUILDS (2026-11-15; docs/STORY-BUILD.md § Hotfix builds, docs/RELEASES.md § Hotfix).
2
+ -- CORE, additive. A hotfix build fixes each repository on its LATEST RELEASE and ports the fix to the
3
+ -- default branch in the same sandbox; Push opens two PRs per repository.
4
+ --
5
+ -- story_builds.hotfix 1 = "Treat as a hotfix" was ticked at Start. 0 = an ordinary build.
6
+ -- story_build_repos.hotfix_tag the latest release (or hotfix) tag the release checkout starts at;
7
+ -- NULL on an ordinary build and on a READ-ONLY repository.
8
+ -- story_build_repos.read_only 1 = no release yet: the agent may read it, its changes are refused.
9
+ -- story_build_repos.main_* the MAIN FIX: the default-branch checkout's base, sandbox path,
10
+ -- patch (+ sha, numbers, files, commit message), its status
11
+ -- (NULL | ready | skipped | failed | pushed) and its push record.
12
+ -- story_build_repos.hotfix_branch_json Push's record of `hotfix/<tag>`: created or found, moved past the
13
+ -- release commit, and the PRs already on it.
14
+ ALTER TABLE `story_builds` ADD COLUMN `hotfix` integer NOT NULL DEFAULT 0;
15
+ --> statement-breakpoint
16
+ ALTER TABLE `story_build_repos` ADD COLUMN `hotfix_tag` text;
17
+ --> statement-breakpoint
18
+ ALTER TABLE `story_build_repos` ADD COLUMN `read_only` integer NOT NULL DEFAULT 0;
19
+ --> statement-breakpoint
20
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_base_ref` text;
21
+ --> statement-breakpoint
22
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_base_sha` text;
23
+ --> statement-breakpoint
24
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_sandbox_path` text;
25
+ --> statement-breakpoint
26
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_patch` text;
27
+ --> statement-breakpoint
28
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_patch_sha` text;
29
+ --> statement-breakpoint
30
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_stat_json` text;
31
+ --> statement-breakpoint
32
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_files_changed_json` text;
33
+ --> statement-breakpoint
34
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_commit_message` text;
35
+ --> statement-breakpoint
36
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_status` text;
37
+ --> statement-breakpoint
38
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_error` text;
39
+ --> statement-breakpoint
40
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_pushed_branch` text;
41
+ --> statement-breakpoint
42
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_pushed_pr_number` integer;
43
+ --> statement-breakpoint
44
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_pushed_pr_url` text;
45
+ --> statement-breakpoint
46
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_pushed_commit_sha` text;
47
+ --> statement-breakpoint
48
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_pushed_at` integer;
49
+ --> statement-breakpoint
50
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_push_error` text;
51
+ --> statement-breakpoint
52
+ ALTER TABLE `story_build_repos` ADD COLUMN `main_push_error_code` text;
53
+ --> statement-breakpoint
54
+ ALTER TABLE `story_build_repos` ADD COLUMN `hotfix_branch_json` text;
@@ -0,0 +1,11 @@
1
+ -- RELEASES: A FAILED RELEASE TAG NO LONGER HOLDS ITS NAME (2026-11-15; docs/RELEASES.md § Implementation).
2
+ -- CORE. The unique (account, repo, release_tag_name) on release_items made a FAILED or REFUSED tag
3
+ -- write hold the name for good: every later press for that version was told "already made". The
4
+ -- name is now held only by a LIVE row (pending / claimed / done) — a partial unique index, declared
5
+ -- here only (like `workspaces_one_default`). A failed or refused row stays as that release's history;
6
+ -- a new press is a new claim on a new row (still claimed before the write, never retried).
7
+ DROP INDEX IF EXISTS `release_items_account_repo_tag`;
8
+ --> statement-breakpoint
9
+ CREATE UNIQUE INDEX IF NOT EXISTS `release_items_account_repo_tag_live` ON `release_items` (`account_id`,`repo_id`,`release_tag_name`) WHERE `tag_state` IN ('pending', 'claimed', 'done');
10
+ --> statement-breakpoint
11
+ CREATE INDEX IF NOT EXISTS `release_items_account_repo_tag_idx` ON `release_items` (`account_id`,`repo_id`,`release_tag_name`);
@@ -0,0 +1,7 @@
1
+ -- IMPLEMENT STORY: THE HOTFIX PORT REPORT (2026-11-15; docs/STORY-BUILD.md § Hotfix builds → The port report,
2
+ -- docs/RELEASES.md § Port report). CORE, additive.
3
+ --
4
+ -- story_build_repos.port_report_json the agent's word on the port to the default branch for this repository:
5
+ -- { outcome: same | adapted | not_needed, reason, evidence[] }, as the host
6
+ -- checked and clipped it. NULL = no report (never inferred). Untrusted text.
7
+ ALTER TABLE `story_build_repos` ADD COLUMN `port_report_json` text;