@snappedly-tools/shipyard 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +46 -43
  2. package/dist/{MountConfig-bZoCs4Dd.d.ts → MountConfig-K5ILnfht.d.ts} +1 -1
  3. package/dist/SandboxProvider-oUAwYlWm.d.ts +120 -0
  4. package/dist/{chunk-FDYOTN55.js → chunk-57EEKW3R.js} +361 -984
  5. package/dist/chunk-57EEKW3R.js.map +1 -0
  6. package/dist/{chunk-56TDSWFU.js → chunk-HXSZM52J.js} +3 -3
  7. package/dist/{chunk-56TDSWFU.js.map → chunk-HXSZM52J.js.map} +1 -1
  8. package/dist/{chunk-WAXEMZUV.js → chunk-JZUBT4WG.js} +241 -33
  9. package/dist/chunk-JZUBT4WG.js.map +1 -0
  10. package/dist/chunk-NQRFVKCU.js +1840 -0
  11. package/dist/chunk-NQRFVKCU.js.map +1 -0
  12. package/dist/chunk-Z5C4LHVP.js +137 -0
  13. package/dist/chunk-Z5C4LHVP.js.map +1 -0
  14. package/dist/createSandbox-DmbnWAZv.d.ts +739 -0
  15. package/dist/index.d.ts +99 -2606
  16. package/dist/index.js +412 -7487
  17. package/dist/index.js.map +1 -1
  18. package/dist/integrations/github.d.ts +52 -0
  19. package/dist/integrations/github.js +1049 -0
  20. package/dist/integrations/github.js.map +1 -0
  21. package/dist/integrations/releases.d.ts +136 -0
  22. package/dist/integrations/releases.js +500 -0
  23. package/dist/integrations/releases.js.map +1 -0
  24. package/dist/main.js +826 -472
  25. package/dist/main.js.map +1 -1
  26. package/dist/publication-BPoy_M9M.d.ts +1200 -0
  27. package/dist/sandboxes/docker.d.ts +2 -3
  28. package/dist/sandboxes/docker.js +2 -4
  29. package/dist/templates/parallel-planner/main.mts +78 -16
  30. package/dist/templates/parallel-planner/planner-branch.mts +151 -0
  31. package/dist/templates/parallel-planner/setup.sh +1 -0
  32. package/dist/templates/parallel-planner-with-review/main.mts +81 -19
  33. package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
  34. package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
  35. package/dist/templates/sequential-reviewer/main.mts +59 -6
  36. package/dist/templates/sequential-reviewer/setup.sh +1 -0
  37. package/dist/templates/shared/setup.sh +1 -0
  38. package/dist/templates/simple-loop/main.mts +58 -5
  39. package/dist/templates/simple-loop/setup.sh +1 -0
  40. package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
  41. package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
  42. package/dist/workflow.d.ts +472 -0
  43. package/dist/workflow.js +4345 -0
  44. package/dist/workflow.js.map +1 -0
  45. package/package.json +11 -15
  46. package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
  47. package/dist/chunk-ACD46ZM4.js +0 -136
  48. package/dist/chunk-ACD46ZM4.js.map +0 -1
  49. package/dist/chunk-FDYOTN55.js.map +0 -1
  50. package/dist/chunk-KMGNFXKN.js +0 -38
  51. package/dist/chunk-KMGNFXKN.js.map +0 -1
  52. package/dist/chunk-SOJTAJTF.js +0 -78
  53. package/dist/chunk-SOJTAJTF.js.map +0 -1
  54. package/dist/chunk-WAXEMZUV.js.map +0 -1
  55. package/dist/sandboxes/no-sandbox.d.ts +0 -37
  56. package/dist/sandboxes/no-sandbox.js +0 -4
  57. package/dist/sandboxes/no-sandbox.js.map +0 -1
  58. package/dist/sandboxes/vercel.d.ts +0 -104
  59. package/dist/sandboxes/vercel.js +0 -166
  60. package/dist/sandboxes/vercel.js.map +0 -1
  61. package/dist/templates/blank/main.mts +0 -13
  62. package/dist/templates/blank/prompt.md +0 -12
  63. package/dist/templates/blank/template.json +0 -4
@@ -0,0 +1,151 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createInterface } from "node:readline/promises";
3
+
4
+ const PLANNER_BRANCH = "shipyard/planner";
5
+
6
+ type BranchPrompt = (message: string) => Promise<string>;
7
+
8
+ const askInTerminal: BranchPrompt = async (message) => {
9
+ const readline = createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ });
13
+ try {
14
+ return await readline.question(message);
15
+ } finally {
16
+ readline.close();
17
+ }
18
+ };
19
+
20
+ const refsConflict = (left: string, right: string): boolean =>
21
+ left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
22
+
23
+ const nextPlannerBranch = (localBranches: readonly string[]): string => {
24
+ for (let suffix = 2; ; suffix++) {
25
+ for (const candidate of [
26
+ `${PLANNER_BRANCH}-${suffix}`,
27
+ `shipyard-planner-${suffix}`,
28
+ ]) {
29
+ if (!localBranches.some((branch) => refsConflict(candidate, branch)))
30
+ return candidate;
31
+ }
32
+ }
33
+ };
34
+
35
+ const reusablePlannerBranch = (
36
+ localBranches: readonly string[],
37
+ ): string | undefined => {
38
+ const candidates = localBranches
39
+ .map((branch) => ({
40
+ branch,
41
+ suffix: /^(?:shipyard\/planner-|shipyard-planner-)(\d+)$/.exec(
42
+ branch,
43
+ )?.[1],
44
+ }))
45
+ .filter((candidate) => candidate.suffix !== undefined)
46
+ .sort((left, right) => Number(left.suffix) - Number(right.suffix));
47
+
48
+ return candidates.find(({ branch }) =>
49
+ localBranches.every(
50
+ (other) => other === branch || !refsConflict(branch, other),
51
+ ),
52
+ )?.branch;
53
+ };
54
+
55
+ export const resolvePlannerBranch = async (
56
+ localBranches: readonly string[],
57
+ options: {
58
+ isInteractive?: boolean;
59
+ ask?: BranchPrompt;
60
+ checkedOutBranches?: readonly string[];
61
+ mergedBranches?: readonly string[];
62
+ deleteBranches?: (branches: readonly string[]) => void;
63
+ } = {},
64
+ ): Promise<string> => {
65
+ const conflicts = localBranches.filter((branch) =>
66
+ refsConflict(PLANNER_BRANCH, branch),
67
+ );
68
+ if (conflicts.length === 0) return PLANNER_BRANCH;
69
+
70
+ const reusable = reusablePlannerBranch(localBranches);
71
+ if (reusable) return reusable;
72
+
73
+ const alternate = nextPlannerBranch(localBranches);
74
+
75
+ const conflictList = conflicts.map((branch) => ` - ${branch}`).join("\n");
76
+ const context =
77
+ `Planner branch '${PLANNER_BRANCH}' conflicts with local branch refs:\n${conflictList}\n` +
78
+ `Git cannot create both names. The listed local branches will be kept unless you choose deletion.`;
79
+ const isInteractive =
80
+ options.isInteractive ??
81
+ Boolean(process.stdin.isTTY && process.stdout.isTTY);
82
+ if (!isInteractive) {
83
+ throw new Error(
84
+ `${context}\nRerun in an interactive terminal to keep them and use '${alternate}', or request local deletion after Git checks that they are merged and not checked out. No branches were changed.`,
85
+ );
86
+ }
87
+
88
+ const checkedOutBranches = new Set(
89
+ options.checkedOutBranches ??
90
+ execFileSync("git", ["worktree", "list", "--porcelain"], {
91
+ encoding: "utf8",
92
+ })
93
+ .split(/\r?\n/)
94
+ .filter((line) => line.startsWith("branch refs/heads/"))
95
+ .map((line) => line.slice("branch refs/heads/".length)),
96
+ );
97
+ const mergedBranches = new Set(
98
+ options.mergedBranches ??
99
+ execFileSync(
100
+ "git",
101
+ ["branch", "--merged", "HEAD", "--format=%(refname:short)"],
102
+ { encoding: "utf8" },
103
+ )
104
+ .split(/\r?\n/)
105
+ .filter(Boolean),
106
+ );
107
+ const branchesSafeToDelete = conflicts.every(
108
+ (branch) => !checkedOutBranches.has(branch) && mergedBranches.has(branch),
109
+ );
110
+ const unsafeDeleteReasons = conflicts.flatMap((branch) => {
111
+ if (checkedOutBranches.has(branch))
112
+ return [`'${branch}' is checked out in a worktree`];
113
+ if (!mergedBranches.has(branch))
114
+ return [`'${branch}' has commits not merged into HEAD`];
115
+ return [];
116
+ });
117
+
118
+ const ask = options.ask ?? askInTerminal;
119
+ const answer = await ask(
120
+ `${context}\n[Y] Keep them and use '${alternate}' (recommended)\n` +
121
+ `[d] Delete these local branches and use '${PLANNER_BRANCH}'${
122
+ branchesSafeToDelete
123
+ ? ""
124
+ : ` (unavailable: ${unsafeDeleteReasons.join("; ")})`
125
+ }\n` +
126
+ `[n] Abort without changes\nChoose [Y/d/n]: `,
127
+ );
128
+ const choice = answer.trim().toLowerCase();
129
+ if (choice === "" || choice === "y" || choice === "yes") return alternate;
130
+ if (choice === "d" || choice === "delete") {
131
+ if (!branchesSafeToDelete) {
132
+ throw new Error(
133
+ `Cannot safely delete the conflicting branches: ${unsafeDeleteReasons.join("; ")}. No branches were changed. Use '${alternate}' or inspect the listed refs manually.`,
134
+ );
135
+ }
136
+ const deleteBranches =
137
+ options.deleteBranches ??
138
+ ((branches: readonly string[]) => {
139
+ for (const branch of branches)
140
+ execFileSync("git", ["branch", "-d", branch], {
141
+ encoding: "utf8",
142
+ });
143
+ });
144
+ deleteBranches(conflicts);
145
+ return PLANNER_BRANCH;
146
+ }
147
+
148
+ throw new Error(
149
+ `Planner branch selection cancelled. No branches were changed. Inspect with 'git worktree list' and 'git branch --list "shipyard/planner*"', then rerun.`,
150
+ );
151
+ };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ # The sandbox starts from a temporary Git bundle, so reset its origin before Git pushes.
4
5
  if [[ -n ${GH_REPO:-} ]]; then
5
6
  [[ "$GH_REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { echo "Invalid GH_REPO" >&2; exit 1; }
6
7
  git remote set-url origin "https://github.com/$GH_REPO.git"
@@ -7,6 +7,55 @@ import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
7
7
 
8
8
  if (process.loadEnvFile && existsSync(".shipyard/.env"))
9
9
  process.loadEnvFile(".shipyard/.env");
10
+ type ModelRole = "routine" | "strong";
11
+ const CODEX_PROVIDER = true;
12
+ const agentFactory = shipyard.codex;
13
+ type AgentModel = Parameters<typeof agentFactory>[0];
14
+ const readRoleModel = (role: ModelRole): string | undefined => {
15
+ const envName = `SHIPYARD_${role.toUpperCase()}_MODEL`;
16
+ const model = process.env[envName];
17
+ if (model !== undefined && model.trim().length === 0)
18
+ throw new Error(`${envName} must not be empty`);
19
+ return model;
20
+ };
21
+ const roleModels = {
22
+ routine: readRoleModel("routine"),
23
+ strong: readRoleModel("strong"),
24
+ };
25
+ const CODEX_REASONING_EFFORTS = shipyard.CODEX_REASONING_EFFORTS;
26
+ type CodexReasoningEffort = shipyard.CodexReasoningEffort;
27
+ const readCodexReasoningEffort = (
28
+ role: ModelRole,
29
+ ): CodexReasoningEffort | undefined => {
30
+ if (!CODEX_PROVIDER) return undefined;
31
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
32
+ const effort = process.env[envName]?.trim();
33
+ if (!effort) return undefined;
34
+ if (!(CODEX_REASONING_EFFORTS as readonly string[]).includes(effort))
35
+ throw new Error(
36
+ `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${effort}"`,
37
+ );
38
+ return effort as CodexReasoningEffort;
39
+ };
40
+ const roleEfforts = {
41
+ routine: readCodexReasoningEffort("routine"),
42
+ strong: readCodexReasoningEffort("strong"),
43
+ };
44
+ const readCodexRoleModel = (role: ModelRole, defaultModel: AgentModel) => {
45
+ if (!CODEX_PROVIDER || typeof defaultModel === "string") return defaultModel;
46
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`;
47
+ const model = process.env[envName]?.trim();
48
+ return model ? { ...defaultModel, model } : defaultModel;
49
+ };
50
+ const roleAgent = (role: ModelRole, defaultModel: AgentModel) => {
51
+ const model = roleModels[role] ?? readCodexRoleModel(role, defaultModel);
52
+ const effort = roleEfforts[role];
53
+ if (typeof model !== "string")
54
+ return effort === undefined
55
+ ? agentFactory(model)
56
+ : agentFactory(model, { effort });
57
+ return agentFactory(model, { effort: effort ?? null });
58
+ };
10
59
  const targetBranch = execFileSync("git", ["branch", "--show-current"], {
11
60
  encoding: "utf8",
12
61
  }).trim();
@@ -21,7 +70,11 @@ if (
21
70
  ) {
22
71
  throw new Error("Invalid target branch or GitHub repository");
23
72
  }
24
- process.env.GH_REPO = repository;
73
+ const sandboxAuthOptions = {};
74
+ const sandboxProvider = docker({
75
+ env: { GH_REPO: repository },
76
+ ...sandboxAuthOptions,
77
+ });
25
78
  const MAX_ITERATIONS = 10;
26
79
  const hooks = {
27
80
  sandbox: {
@@ -126,7 +179,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
126
179
  ]);
127
180
  const sandbox = await shipyard.createSandbox({
128
181
  branch: issue.branch,
129
- sandbox: docker(),
182
+ sandbox: sandboxProvider,
130
183
  hooks,
131
184
  });
132
185
  let evidence: string;
@@ -136,7 +189,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
136
189
  : [issue.id]) {
137
190
  await sandbox.run({
138
191
  name: `triage #${ticketId}`,
139
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
192
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
140
193
  maxIterations: 1,
141
194
  promptFile: "./.shipyard/triage-prompt.md",
142
195
  promptArgs: { TASK_ID: ticketId },
@@ -146,7 +199,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
146
199
  const implement = await sandbox.run({
147
200
  name: "implementer",
148
201
  maxIterations: 1,
149
- agent: shipyard.codex(shipyard.CODEX_MODELS.routine),
202
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
150
203
  promptFile: "./.shipyard/implement-prompt.md",
151
204
  promptArgs: {
152
205
  TASK_ID: issue.id,
@@ -165,7 +218,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
165
218
  const review = await sandbox.run({
166
219
  name: "reviewer",
167
220
  maxIterations: 1,
168
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
221
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
169
222
  promptFile: "./.shipyard/review-prompt.md",
170
223
  promptArgs: {
171
224
  BRANCH: issue.branch,
@@ -191,7 +244,7 @@ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
191
244
 
192
245
  const publication = await shipyard.createSandbox({
193
246
  branch: issue.branch,
194
- sandbox: docker(),
247
+ sandbox: sandboxProvider,
195
248
  });
196
249
  try {
197
250
  publicationUncertain = true;
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ # The sandbox starts from a temporary Git bundle, so reset its origin before Git pushes.
4
5
  if [[ -n ${GH_REPO:-} ]]; then
5
6
  [[ "$GH_REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { echo "Invalid GH_REPO" >&2; exit 1; }
6
7
  git remote set-url origin "https://github.com/$GH_REPO.git"
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ # The sandbox starts from a temporary Git bundle, so reset its origin before Git pushes.
4
5
  if [[ -n ${GH_REPO:-} ]]; then
5
6
  [[ "$GH_REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { echo "Invalid GH_REPO" >&2; exit 1; }
6
7
  git remote set-url origin "https://github.com/$GH_REPO.git"
@@ -7,6 +7,55 @@ import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
7
7
 
8
8
  if (process.loadEnvFile && existsSync(".shipyard/.env"))
9
9
  process.loadEnvFile(".shipyard/.env");
10
+ type ModelRole = "routine" | "strong";
11
+ const CODEX_PROVIDER = true;
12
+ const agentFactory = shipyard.codex;
13
+ type AgentModel = Parameters<typeof agentFactory>[0];
14
+ const readRoleModel = (role: ModelRole): string | undefined => {
15
+ const envName = `SHIPYARD_${role.toUpperCase()}_MODEL`;
16
+ const model = process.env[envName];
17
+ if (model !== undefined && model.trim().length === 0)
18
+ throw new Error(`${envName} must not be empty`);
19
+ return model;
20
+ };
21
+ const roleModels = {
22
+ routine: readRoleModel("routine"),
23
+ strong: readRoleModel("strong"),
24
+ };
25
+ const CODEX_REASONING_EFFORTS = shipyard.CODEX_REASONING_EFFORTS;
26
+ type CodexReasoningEffort = shipyard.CodexReasoningEffort;
27
+ const readCodexReasoningEffort = (
28
+ role: ModelRole,
29
+ ): CodexReasoningEffort | undefined => {
30
+ if (!CODEX_PROVIDER) return undefined;
31
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
32
+ const effort = process.env[envName]?.trim();
33
+ if (!effort) return undefined;
34
+ if (!(CODEX_REASONING_EFFORTS as readonly string[]).includes(effort))
35
+ throw new Error(
36
+ `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${effort}"`,
37
+ );
38
+ return effort as CodexReasoningEffort;
39
+ };
40
+ const roleEfforts = {
41
+ routine: readCodexReasoningEffort("routine"),
42
+ strong: readCodexReasoningEffort("strong"),
43
+ };
44
+ const readCodexRoleModel = (role: ModelRole, defaultModel: AgentModel) => {
45
+ if (!CODEX_PROVIDER || typeof defaultModel === "string") return defaultModel;
46
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`;
47
+ const model = process.env[envName]?.trim();
48
+ return model ? { ...defaultModel, model } : defaultModel;
49
+ };
50
+ const roleAgent = (role: ModelRole, defaultModel: AgentModel) => {
51
+ const model = roleModels[role] ?? readCodexRoleModel(role, defaultModel);
52
+ const effort = roleEfforts[role];
53
+ if (typeof model !== "string")
54
+ return effort === undefined
55
+ ? agentFactory(model)
56
+ : agentFactory(model, { effort });
57
+ return agentFactory(model, { effort: effort ?? null });
58
+ };
10
59
  const targetBranch = execFileSync("git", ["branch", "--show-current"], {
11
60
  encoding: "utf8",
12
61
  }).trim();
@@ -21,7 +70,11 @@ if (
21
70
  ) {
22
71
  throw new Error("Invalid target branch or GitHub repository");
23
72
  }
24
- process.env.GH_REPO = repository;
73
+ const sandboxAuthOptions = {};
74
+ const sandboxProvider = docker({
75
+ env: { GH_REPO: repository },
76
+ ...sandboxAuthOptions,
77
+ });
25
78
  const hooks = {
26
79
  sandbox: {
27
80
  onSandboxReady: [
@@ -123,7 +176,7 @@ for (let iteration = 0; iteration < 3; iteration++) {
123
176
  ]);
124
177
  const sandbox = await shipyard.createSandbox({
125
178
  branch: issue.branch,
126
- sandbox: docker(),
179
+ sandbox: sandboxProvider,
127
180
  hooks,
128
181
  });
129
182
  let evidence: string;
@@ -133,7 +186,7 @@ for (let iteration = 0; iteration < 3; iteration++) {
133
186
  : [issue.id]) {
134
187
  await sandbox.run({
135
188
  name: `triage #${ticketId}`,
136
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
189
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
137
190
  maxIterations: 1,
138
191
  promptFile: "./.shipyard/triage-prompt.md",
139
192
  promptArgs: { TASK_ID: ticketId },
@@ -142,7 +195,7 @@ for (let iteration = 0; iteration < 3; iteration++) {
142
195
  }
143
196
  const result = await sandbox.run({
144
197
  name: "implementer",
145
- agent: shipyard.codex(shipyard.CODEX_MODELS.routine),
198
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
146
199
  maxIterations: 1,
147
200
  promptFile: "./.shipyard/prompt.md",
148
201
  promptArgs: {
@@ -171,7 +224,7 @@ for (let iteration = 0; iteration < 3; iteration++) {
171
224
  // branch so a later invocation can fast-forward the same PR.
172
225
  const publication = await shipyard.createSandbox({
173
226
  branch: issue.branch,
174
- sandbox: docker(),
227
+ sandbox: sandboxProvider,
175
228
  });
176
229
  try {
177
230
  publicationUncertain = true;
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ # The sandbox starts from a temporary Git bundle, so reset its origin before Git pushes.
4
5
  if [[ -n ${GH_REPO:-} ]]; then
5
6
  [[ "$GH_REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { echo "Invalid GH_REPO" >&2; exit 1; }
6
7
  git remote set-url origin "https://github.com/$GH_REPO.git"
@@ -0,0 +1,11 @@
1
+ -- Durable records for triage investigations and bounded repair publication.
2
+ -- These records resume phase-specific work; the coordinator ledger remains
3
+ -- responsible for job, dispatch, lease, and event state.
4
+
5
+ CREATE TABLE IF NOT EXISTS shipyard_workflow_phase_records (
6
+ namespace TEXT NOT NULL CHECK (namespace IN ('triage', 'repair-batch')),
7
+ record_key TEXT NOT NULL,
8
+ record JSONB NOT NULL,
9
+ updated_at TIMESTAMPTZ NOT NULL,
10
+ PRIMARY KEY (namespace, record_key)
11
+ );
@@ -0,0 +1,6 @@
1
+ -- Version phase records without breaking writers from the previous release.
2
+ -- The default assigns old-format inserts version 1; code rollback can leave the
3
+ -- additive column in place.
4
+ ALTER TABLE shipyard_workflow_phase_records
5
+ ADD COLUMN IF NOT EXISTS schema_version INTEGER NOT NULL DEFAULT 1
6
+ CHECK (schema_version = 1);