@treeseed/sdk 0.6.32 → 0.6.33

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.
@@ -162,8 +162,9 @@ export declare function mergeStagingIntoMain(cwd?: any): {
162
162
  commitSha: string;
163
163
  pushed: boolean;
164
164
  };
165
- export declare function mergeBranchIntoTarget(cwd?: any, { sourceBranch, targetBranch, message, pushTarget }?: {
165
+ export declare function mergeBranchIntoTarget(cwd?: any, { sourceBranch, targetBranch, message, pushTarget, quietMerge }?: {
166
166
  pushTarget?: boolean | undefined;
167
+ quietMerge?: boolean | undefined;
167
168
  }): {
168
169
  repoDir: string;
169
170
  targetBranch: any;
@@ -425,13 +425,13 @@ function mergeStagingIntoMain(cwd = workspaceRoot()) {
425
425
  pushTarget: true
426
426
  });
427
427
  }
428
- function mergeBranchIntoTarget(cwd = workspaceRoot(), { sourceBranch, targetBranch, message, pushTarget = true } = {}) {
428
+ function mergeBranchIntoTarget(cwd = workspaceRoot(), { sourceBranch, targetBranch, message, pushTarget = true, quietMerge = false } = {}) {
429
429
  const repoDir = prepareReleaseBranches(cwd);
430
430
  checkoutBranch(repoDir, targetBranch);
431
431
  if (remoteBranchExists(repoDir, targetBranch)) {
432
432
  runGit(["merge", "--ff-only", `origin/${targetBranch}`], { cwd: repoDir });
433
433
  }
434
- runGit(["merge", "--no-ff", sourceBranch, "-m", message], { cwd: repoDir });
434
+ runGit(["merge", "--no-ff", sourceBranch, "-m", message], { cwd: repoDir, capture: quietMerge });
435
435
  pushBranch(repoDir, STAGING_BRANCH);
436
436
  if (pushTarget) {
437
437
  pushBranch(repoDir, targetBranch);
@@ -431,6 +431,21 @@ function formatElapsed(seconds) {
431
431
  function shortSha(value) {
432
432
  return value ? value.slice(0, 12) : "(unknown)";
433
433
  }
434
+ function activeJobSummary(event) {
435
+ const activeJobs = event.activeJobs ?? [];
436
+ if (activeJobs.length === 0) return "";
437
+ const summaries = activeJobs.slice(0, 2).map((job) => {
438
+ const activeStep = (job.steps ?? []).find((step) => step.status && step.status !== "completed");
439
+ return activeStep?.name ? `${job.name} > ${activeStep.name}` : job.name;
440
+ }).filter(Boolean);
441
+ return summaries.length > 0 ? `; active: ${summaries.join(", ")}` : "";
442
+ }
443
+ function failedJobSummary(event) {
444
+ const failedJobs = event.failedJobs ?? [];
445
+ if (failedJobs.length === 0) return "";
446
+ const names = failedJobs.slice(0, 3).map((job) => job.name).filter(Boolean);
447
+ return names.length > 0 ? `; failed: ${names.join(", ")}` : "";
448
+ }
434
449
  function formatGitHubActionsGateProgress(gate, event, operation) {
435
450
  const prefix = `[${operation}][gate][${gate.name}] ${event.workflow}`;
436
451
  if (event.type === "waiting") {
@@ -439,12 +454,12 @@ function formatGitHubActionsGateProgress(gate, event, operation) {
439
454
  if (event.type === "completed") {
440
455
  const conclusion = event.conclusion === "success" ? "successfully" : `with conclusion ${event.conclusion ?? "unknown"}`;
441
456
  const url2 = event.url ? `: ${event.url}` : "";
442
- return `${prefix} completed ${conclusion} in ${formatElapsed(event.elapsedSeconds)}${url2}`;
457
+ return `${prefix} completed ${conclusion}${failedJobSummary(event)} in ${formatElapsed(event.elapsedSeconds)}${url2}`;
443
458
  }
444
459
  const status = event.status ?? "waiting";
445
460
  const url = event.url ? `: ${event.url}` : "";
446
461
  const run = event.runId ? ` run ${event.runId}` : "";
447
- return `${prefix}${run} ${status}${url} (${formatElapsed(event.elapsedSeconds)} elapsed)`;
462
+ return `${prefix}${run} ${status}${activeJobSummary(event)}${url} (${formatElapsed(event.elapsedSeconds)} elapsed)`;
448
463
  }
449
464
  async function waitForGitHubActionsGate(gate, options = {}) {
450
465
  const { waitForGitHubWorkflowCompletion } = await import("./github-automation.js");
@@ -33,6 +33,12 @@ export interface GitHubWorkflowJobSummary {
33
33
  status: string | null;
34
34
  conclusion: string | null;
35
35
  url: string | null;
36
+ steps?: GitHubWorkflowJobStepSummary[];
37
+ }
38
+ export interface GitHubWorkflowJobStepSummary {
39
+ name: string;
40
+ status: string | null;
41
+ conclusion: string | null;
36
42
  }
37
43
  export type GitHubWorkflowProgressEvent = {
38
44
  type: 'waiting' | 'running' | 'completed';
@@ -45,6 +51,10 @@ export type GitHubWorkflowProgressEvent = {
45
51
  url: string | null;
46
52
  status: string | null;
47
53
  conclusion: string | null;
54
+ jobs?: GitHubWorkflowJobSummary[];
55
+ activeJobs?: GitHubWorkflowJobSummary[];
56
+ completedJobs?: GitHubWorkflowJobSummary[];
57
+ failedJobs?: GitHubWorkflowJobSummary[];
48
58
  };
49
59
  export declare function resolveGitHubApiToken(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
50
60
  export declare function parseGitHubRepositorySlug(value: string): {
@@ -529,9 +529,27 @@ function normalizeWorkflowJob(job) {
529
529
  name: String(job.name ?? ""),
530
530
  status: typeof job.status === "string" ? job.status : null,
531
531
  conclusion: typeof job.conclusion === "string" ? job.conclusion : null,
532
- url: typeof job.html_url === "string" ? job.html_url : null
532
+ url: typeof job.html_url === "string" ? job.html_url : null,
533
+ steps: Array.isArray(job.steps) ? job.steps.map((step) => ({
534
+ name: String(step.name ?? ""),
535
+ status: typeof step.status === "string" ? step.status : null,
536
+ conclusion: typeof step.conclusion === "string" ? step.conclusion : null
537
+ })) : []
533
538
  };
534
539
  }
540
+ async function listWorkflowJobsForProgress(client, owner, repo, runId) {
541
+ try {
542
+ const jobs = await client.rest.actions.listJobsForWorkflowRun({
543
+ owner,
544
+ repo,
545
+ run_id: runId,
546
+ per_page: 100
547
+ });
548
+ return jobs.data.jobs.map((job) => normalizeWorkflowJob(job));
549
+ } catch {
550
+ return [];
551
+ }
552
+ }
535
553
  function sleep(ms) {
536
554
  return new Promise((resolve) => setTimeout(resolve, ms));
537
555
  }
@@ -547,7 +565,10 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
547
565
  const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
548
566
  const startedAt = Date.now();
549
567
  let lastProgress = null;
550
- const emitProgress = (type, run = null) => {
568
+ const emitProgress = (type, run = null, jobs = []) => {
569
+ const completedJobs = jobs.filter((job) => job.status === "completed");
570
+ const failedJobs = jobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped");
571
+ const activeJobs = jobs.filter((job) => job.status && job.status !== "completed");
551
572
  const event = {
552
573
  type,
553
574
  repository: `${owner}/${name}`,
@@ -558,7 +579,11 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
558
579
  runId: run?.id ?? null,
559
580
  url: run?.url ?? null,
560
581
  status: run?.status ?? null,
561
- conclusion: run?.conclusion ?? null
582
+ conclusion: run?.conclusion ?? null,
583
+ jobs,
584
+ activeJobs,
585
+ completedJobs,
586
+ failedJobs
562
587
  };
563
588
  lastProgress = event;
564
589
  onProgress?.(event);
@@ -587,15 +612,10 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
587
612
  run_id: match.id
588
613
  });
589
614
  const normalized = normalizeWorkflowRun(current.data);
615
+ const progressJobs = await listWorkflowJobsForProgress(client, owner, name, match.id);
590
616
  if (normalized.status === "completed") {
591
- emitProgress("completed", normalized);
592
- const jobs = await client.rest.actions.listJobsForWorkflowRun({
593
- owner,
594
- repo: name,
595
- run_id: match.id,
596
- per_page: 100
597
- });
598
- const normalizedJobs = jobs.data.jobs.map((job) => normalizeWorkflowJob(job));
617
+ const normalizedJobs = progressJobs;
618
+ emitProgress("completed", normalized, normalizedJobs);
599
619
  return {
600
620
  status: "completed",
601
621
  repository: `${owner}/${name}`,
@@ -609,7 +629,7 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
609
629
  failedJobs: normalizedJobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped")
610
630
  };
611
631
  }
612
- emitProgress("running", normalized);
632
+ emitProgress("running", normalized, progressJobs);
613
633
  await sleep(pollSeconds * 1e3);
614
634
  }
615
635
  } catch (error) {
@@ -4,11 +4,19 @@ import { readFileSync } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
5
 
6
6
  const args = process.argv.slice(2);
7
+ const defaultAllowlisted = [
8
+ /Module "url" has been externalized for browser compatibility, imported by ".*libsodium-sumo.*"/u,
9
+ ];
7
10
  const allowlisted = [];
8
11
  const files = [];
12
+ let useDefaultPolicy = true;
9
13
 
10
14
  for (let index = 0; index < args.length; index += 1) {
11
15
  const arg = args[index];
16
+ if (arg === '--no-default-policy') {
17
+ useDefaultPolicy = false;
18
+ continue;
19
+ }
12
20
  if (arg === '--allow') {
13
21
  const pattern = args[index + 1];
14
22
  if (!pattern) {
@@ -22,17 +30,21 @@ for (let index = 0; index < args.length; index += 1) {
22
30
  }
23
31
 
24
32
  if (files.length === 0) {
25
- throw new Error('Usage: node check-build-warnings.mjs <log-file> [<log-file> ...] [--allow <regex>]');
33
+ throw new Error('Usage: node check-build-warnings.mjs <log-file> [<log-file> ...] [--allow <regex>] [--no-default-policy]');
26
34
  }
27
35
 
28
36
  const warningLines = [];
37
+ const effectiveAllowlisted = [
38
+ ...(useDefaultPolicy ? defaultAllowlisted : []),
39
+ ...allowlisted,
40
+ ];
29
41
  for (const file of files) {
30
42
  const contents = readFileSync(resolve(process.cwd(), file), 'utf8');
31
43
  for (const line of contents.split(/\r?\n/u)) {
32
44
  if (!line.includes('[WARN]')) {
33
45
  continue;
34
46
  }
35
- if (allowlisted.some((pattern) => pattern.test(line))) {
47
+ if (effectiveAllowlisted.some((pattern) => pattern.test(line))) {
36
48
  continue;
37
49
  }
38
50
  warningLines.push(line);
@@ -798,6 +798,8 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
798
798
  worktreePath: string | null;
799
799
  primaryRoot: string | null;
800
800
  ciMode: "hosted" | "off";
801
+ fresh: boolean;
802
+ freshArchivedRuns: never[];
801
803
  mode: TreeseedWorkflowMode;
802
804
  mergeStrategy: string;
803
805
  level: string;
@@ -844,6 +846,11 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
844
846
  mode: "root-only";
845
847
  mergeStrategy: string;
846
848
  level: "patch" | "major" | "minor";
849
+ fresh: boolean;
850
+ freshArchivedRuns: {
851
+ runId: string;
852
+ reasons: string[];
853
+ }[];
847
854
  resumed: boolean;
848
855
  resumedRunId: string | null;
849
856
  autoResumed: boolean;
@@ -909,6 +916,11 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
909
916
  mode: "recursive-workspace";
910
917
  mergeStrategy: string;
911
918
  level: "patch" | "major" | "minor";
919
+ fresh: boolean;
920
+ freshArchivedRuns: {
921
+ runId: string;
922
+ reasons: string[];
923
+ }[];
912
924
  resumed: boolean;
913
925
  resumedRunId: string | null;
914
926
  autoResumed: boolean;
@@ -1041,6 +1053,11 @@ export declare function workflowRecover(helpers: WorkflowOperationHelpers, input
1041
1053
  } | null;
1042
1054
  classification: import("./runs.ts").TreeseedWorkflowRunClassification;
1043
1055
  }[];
1056
+ markedObsoleteRun: {
1057
+ runId: string;
1058
+ command: TreeseedWorkflowRunCommand;
1059
+ reason: string;
1060
+ } | null;
1044
1061
  selectedRun: TreeseedWorkflowRunJournal | null;
1045
1062
  runCount: number;
1046
1063
  } & {
@@ -254,13 +254,30 @@ function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
254
254
  const packagePaths = new Set(packages.map((pkg) => pkg.repoPath));
255
255
  const unresolved = unresolvedMergePaths(gitRoot);
256
256
  if (unresolved.length === 0 || unresolved.some((filePath) => !packagePaths.has(filePath))) {
257
- return false;
257
+ return {
258
+ resolved: false,
259
+ allUnresolvedPathsWerePackagePointers: unresolved.length > 0 && unresolved.every((filePath) => packagePaths.has(filePath)),
260
+ unresolvedPaths: unresolved,
261
+ entries: []
262
+ };
258
263
  }
264
+ const entries = [];
259
265
  for (const pkg of packages) {
260
266
  syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
261
267
  run("git", ["add", pkg.repoPath], { cwd: gitRoot });
268
+ entries.push({
269
+ packageName: pkg.name,
270
+ path: pkg.repoPath,
271
+ targetBranch: PRODUCTION_BRANCH,
272
+ resolvedCommit: headCommit(pkg.dir)
273
+ });
262
274
  }
263
- return true;
275
+ return {
276
+ resolved: true,
277
+ allUnresolvedPathsWerePackagePointers: true,
278
+ unresolvedPaths: unresolved,
279
+ entries
280
+ };
264
281
  }
265
282
  function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
266
283
  if (!shouldManageWorkspaceLinks(mode, helpers.context.env)) {
@@ -1060,6 +1077,123 @@ function releasePlanMatchesCurrentHeads(plan, rootRepo, packageReports) {
1060
1077
  function releaseRunHasCompletedMutation(journal) {
1061
1078
  return journal.steps.some((step) => step.status === "completed" && step.id !== "release-plan" && step.id !== "workspace-unlink");
1062
1079
  }
1080
+ function generatedReleaseMetadataFiles(repoDir) {
1081
+ return ["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((filePath) => {
1082
+ if (existsSync(resolve(repoDir, filePath))) return true;
1083
+ try {
1084
+ run("git", ["ls-files", "--error-unmatch", filePath], { cwd: repoDir, capture: true });
1085
+ return true;
1086
+ } catch {
1087
+ return false;
1088
+ }
1089
+ });
1090
+ }
1091
+ function collectReleaseCleanupSnapshot(root, selectedPackageNames) {
1092
+ return {
1093
+ repos: [
1094
+ {
1095
+ name: "@treeseed/market",
1096
+ path: repoRoot(root),
1097
+ branch: currentBranch(repoRoot(root)) || null,
1098
+ files: generatedReleaseMetadataFiles(repoRoot(root))
1099
+ },
1100
+ ...checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageNames.has(pkg.name)).map((pkg) => ({
1101
+ name: pkg.name,
1102
+ path: pkg.dir,
1103
+ branch: currentBranch(pkg.dir) || null,
1104
+ files: generatedReleaseMetadataFiles(pkg.dir)
1105
+ }))
1106
+ ]
1107
+ };
1108
+ }
1109
+ function restoreReleaseGeneratedMetadata(repo) {
1110
+ const restored = [];
1111
+ const skipped = [];
1112
+ for (const filePath of repo.files) {
1113
+ const status = run("git", ["status", "--porcelain", "--", filePath], { cwd: repo.path, capture: true });
1114
+ if (!status.trim()) {
1115
+ skipped.push(filePath);
1116
+ continue;
1117
+ }
1118
+ run("git", ["restore", "--staged", "--worktree", "--", filePath], { cwd: repo.path, capture: true });
1119
+ restored.push(filePath);
1120
+ }
1121
+ return { restored, skipped };
1122
+ }
1123
+ function cleanupFailedReleaseLocalState(root, helpers, snapshot, workspaceLinksMode) {
1124
+ const report = { restored: [], skipped: [], manualReview: [] };
1125
+ try {
1126
+ ensureWorkflowWorkspaceLinks(root, helpers, workspaceLinksMode ?? "auto");
1127
+ } catch (error) {
1128
+ report.manualReview.push({
1129
+ scope: "workspace-links",
1130
+ reason: error instanceof Error ? error.message : String(error)
1131
+ });
1132
+ }
1133
+ if (!snapshot) {
1134
+ report.skipped.push({ scope: "release-metadata", reason: "cleanup snapshot was not recorded before failure" });
1135
+ return report;
1136
+ }
1137
+ for (const repo of snapshot.repos) {
1138
+ try {
1139
+ const restored = restoreReleaseGeneratedMetadata(repo);
1140
+ if (repo.branch && currentBranch(repo.path) !== repo.branch) {
1141
+ checkoutBranch(repo.path, repo.branch);
1142
+ }
1143
+ if (restored.restored.length > 0) {
1144
+ report.restored.push({ repo: repo.name, path: repo.path, files: restored.restored });
1145
+ }
1146
+ if (restored.skipped.length > 0) {
1147
+ report.skipped.push({ repo: repo.name, path: repo.path, files: restored.skipped, reason: "unchanged" });
1148
+ }
1149
+ } catch (error) {
1150
+ report.manualReview.push({
1151
+ repo: repo.name,
1152
+ path: repo.path,
1153
+ branch: repo.branch,
1154
+ files: repo.files,
1155
+ reason: error instanceof Error ? error.message : String(error),
1156
+ nextCommand: repo.branch ? `git -C ${repo.path} restore --staged --worktree -- ${repo.files.join(" ")} && git -C ${repo.path} checkout ${repo.branch}` : null
1157
+ });
1158
+ }
1159
+ }
1160
+ return report;
1161
+ }
1162
+ function prepareFreshReleaseRun(root, branch, rootRepo, packageReports) {
1163
+ if (branch !== STAGING_BRANCH) return { archived: [], blockers: [] };
1164
+ const currentHeads = Object.fromEntries([
1165
+ [rootRepo.name, rootRepo.commitSha ?? null],
1166
+ ...packageReports.map((report) => [report.name, report.commitSha ?? null])
1167
+ ]);
1168
+ const archived = [];
1169
+ const blockers = [];
1170
+ for (const journal of listInterruptedWorkflowRuns(root).filter((entry) => entry.command === "release")) {
1171
+ const classification = classifyWorkflowRunJournal(journal, {
1172
+ currentBranch: branch,
1173
+ currentHeads
1174
+ });
1175
+ if (classification.state === "stale") {
1176
+ archiveWorkflowRun(root, journal.runId, {
1177
+ ...classification,
1178
+ reasons: ["fresh release superseded stale failed release", ...classification.reasons]
1179
+ });
1180
+ archived.push({ runId: journal.runId, reasons: classification.reasons });
1181
+ continue;
1182
+ }
1183
+ if (classification.state === "resumable" && releaseRunHasCompletedMutation(journal)) {
1184
+ blockers.push(`${journal.runId}: completed release mutations and is still safe to resume. Mark it obsolete with \`npx trsd recover --obsolete ${journal.runId} --reason "superseded by fresh release"\` before using --fresh.`);
1185
+ }
1186
+ }
1187
+ if (blockers.length > 0) {
1188
+ workflowError("release", "validation_failed", [
1189
+ "Treeseed release --fresh will not bypass a resumable partial release that already completed release mutations.",
1190
+ ...blockers
1191
+ ].join("\n"), {
1192
+ details: { archived, blockers }
1193
+ });
1194
+ }
1195
+ return { archived, blockers };
1196
+ }
1063
1197
  function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports) {
1064
1198
  if (branch !== STAGING_BRANCH) return null;
1065
1199
  return listInterruptedWorkflowRuns(root).find((journal) => {
@@ -3323,8 +3457,10 @@ async function workflowRelease(helpers, input) {
3323
3457
  const rootRepo = createWorkspaceRootRepoReport(root);
3324
3458
  const packageReports = createWorkspacePackageReports(root);
3325
3459
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
3326
- const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
3327
- const planAutoResumeRun = executionMode === "plan" ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
3460
+ const freshRelease = input.fresh === true && !explicitResumeRunId;
3461
+ const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
3462
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
3463
+ const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
3328
3464
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
3329
3465
  const level = effectiveInput.bump ?? "patch";
3330
3466
  const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
@@ -3345,6 +3481,8 @@ async function workflowRelease(helpers, input) {
3345
3481
  return buildWorkflowResult("release", root, {
3346
3482
  ...plannedRelease,
3347
3483
  ciMode,
3484
+ fresh: input.fresh === true,
3485
+ freshArchivedRuns: [],
3348
3486
  ...worktreePayload(root, effectiveInput.worktreeMode),
3349
3487
  autoResumeCandidate: planAutoResumeRun ? {
3350
3488
  runId: planAutoResumeRun.runId,
@@ -3372,6 +3510,7 @@ async function workflowRelease(helpers, input) {
3372
3510
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
3373
3511
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
3374
3512
  ciMode,
3513
+ fresh: input.fresh === true,
3375
3514
  worktreeMode: effectiveInput.worktreeMode ?? "auto",
3376
3515
  workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
3377
3516
  },
@@ -3404,6 +3543,7 @@ async function workflowRelease(helpers, input) {
3404
3543
  if (autoResumeRun) {
3405
3544
  helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
3406
3545
  }
3546
+ let releaseCleanupSnapshot = null;
3407
3547
  try {
3408
3548
  const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
3409
3549
  const effectivePackageSelection = releasePlanPackageSelection(releasePlan.packageSelection);
@@ -3491,6 +3631,8 @@ async function workflowRelease(helpers, input) {
3491
3631
  mode,
3492
3632
  mergeStrategy: "merge-commit",
3493
3633
  level,
3634
+ fresh: input.fresh === true,
3635
+ freshArchivedRuns: freshPreparation.archived,
3494
3636
  resumed: workflowRun.resumed,
3495
3637
  resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
3496
3638
  autoResumed: autoResumeRun != null,
@@ -3527,6 +3669,7 @@ async function workflowRelease(helpers, input) {
3527
3669
  prepareReleaseBranches(pkg.dir);
3528
3670
  }
3529
3671
  }
3672
+ releaseCleanupSnapshot = collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
3530
3673
  const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
3531
3674
  const releasedPackageDevTags2 = Object.fromEntries(
3532
3675
  checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
@@ -3641,17 +3784,22 @@ async function workflowRelease(helpers, input) {
3641
3784
  pushBranch(gitRoot, STAGING_BRANCH);
3642
3785
  const stagingCommit = headCommit(gitRoot);
3643
3786
  let released;
3787
+ let submoduleReconciliation = null;
3644
3788
  try {
3645
3789
  released = mergeBranchIntoTarget(root, {
3646
3790
  sourceBranch: STAGING_BRANCH,
3647
3791
  targetBranch: PRODUCTION_BRANCH,
3648
3792
  message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
3649
- pushTarget: false
3793
+ pushTarget: false,
3794
+ quietMerge: true
3650
3795
  });
3651
3796
  } catch (error) {
3652
- if (!resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames)) {
3797
+ const reconciliation = resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames);
3798
+ if (!reconciliation.resolved) {
3653
3799
  throw error;
3654
3800
  }
3801
+ helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
3802
+ submoduleReconciliation = reconciliation;
3655
3803
  commitAllIfChanged(gitRoot, `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`);
3656
3804
  released = { commitSha: headCommit(gitRoot) };
3657
3805
  }
@@ -3671,7 +3819,8 @@ async function workflowRelease(helpers, input) {
3671
3819
  stagingCommit,
3672
3820
  releasedCommit,
3673
3821
  mergeCommit: released.commitSha,
3674
- tag
3822
+ tag,
3823
+ submoduleReconciliation
3675
3824
  };
3676
3825
  });
3677
3826
  rootRepo.committed = true;
@@ -3752,6 +3901,8 @@ async function workflowRelease(helpers, input) {
3752
3901
  mode,
3753
3902
  mergeStrategy: "merge-commit",
3754
3903
  level,
3904
+ fresh: input.fresh === true,
3905
+ freshArchivedRuns: freshPreparation.archived,
3755
3906
  resumed: workflowRun.resumed,
3756
3907
  resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
3757
3908
  autoResumed: autoResumeRun != null,
@@ -3792,7 +3943,7 @@ async function workflowRelease(helpers, input) {
3792
3943
  ])
3793
3944
  });
3794
3945
  } catch (error) {
3795
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
3946
+ const localCleanup = cleanupFailedReleaseLocalState(root, helpers, releaseCleanupSnapshot, effectiveInput.workspaceLinks ?? "auto");
3796
3947
  const latestJournal = readWorkflowRunJournal(root, workflowRun.runId);
3797
3948
  const lastCompleted = [...latestJournal?.steps ?? []].reverse().find((step) => step.status === "completed") ?? null;
3798
3949
  const nextPending = latestJournal?.steps.find((step) => step.status === "pending") ?? null;
@@ -3808,14 +3959,22 @@ ${repair.blockers.join("\n")}`, "stderr");
3808
3959
  } catch (repairError) {
3809
3960
  helpers.write(`[release][recovery] Package repo repair failed: ${repairError instanceof Error ? repairError.message : String(repairError)}`, "stderr");
3810
3961
  }
3811
- helpers.write(`Safe recovery: npx trsd release --${level} --json, or inspect with npx trsd recover --json.`, "stderr");
3962
+ if (localCleanup.restored.length > 0) {
3963
+ helpers.write(`[release][recovery] Restored generated release metadata in ${localCleanup.restored.length} repo(s).`, "stderr");
3964
+ }
3965
+ if (localCleanup.manualReview.length > 0) {
3966
+ helpers.write(`[release][recovery] Local cleanup needs manual review:
3967
+ ${localCleanup.manualReview.map((entry) => `- ${String(entry.repo ?? entry.scope ?? "repo")}: ${String(entry.reason ?? "unknown")}`).join("\n")}`, "stderr");
3968
+ }
3969
+ helpers.write(`Safe recovery: npx trsd release --${level} --json, npx trsd release --${level} --fresh --json, or inspect with npx trsd recover --json.`, "stderr");
3812
3970
  failWorkflowRun(root, workflowRun.runId, error, {
3813
3971
  resumable: true,
3814
3972
  runId: workflowRun.runId,
3815
3973
  command: "release",
3816
3974
  message: `Resume the interrupted release on ${STAGING_BRANCH}. Last phase: ${lastCompleted?.id ?? "not-started"}; next phase: ${nextPending?.id ?? "none"}.`,
3817
3975
  recoverCommand: "npx trsd recover --json",
3818
- resumeCommand: `npx trsd release --${level} --json`
3976
+ resumeCommand: `npx trsd release --${level} --json`,
3977
+ localCleanup
3819
3978
  });
3820
3979
  throw error;
3821
3980
  }
@@ -3909,7 +4068,29 @@ async function workflowRecover(helpers, input = {}) {
3909
4068
  currentBranch: session.branchName,
3910
4069
  currentHeads
3911
4070
  });
3912
- const interruptedRuns = classifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
4071
+ const markedObsoleteRun = input.obsoleteRunId ? (() => {
4072
+ const entry = classifiedRuns.find((candidate) => candidate.journal.runId === input.obsoleteRunId);
4073
+ if (!entry) {
4074
+ workflowError("recover", "validation_failed", `Treeseed recover could not find workflow run ${input.obsoleteRunId}.`);
4075
+ }
4076
+ const reason = input.obsoleteReason?.trim() || "marked obsolete by operator";
4077
+ const classification = {
4078
+ state: "obsolete",
4079
+ reasons: [reason],
4080
+ classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
4081
+ };
4082
+ archiveWorkflowRun(root, entry.journal.runId, classification);
4083
+ return {
4084
+ runId: entry.journal.runId,
4085
+ command: entry.journal.command,
4086
+ reason
4087
+ };
4088
+ })() : null;
4089
+ const effectiveClassifiedRuns = markedObsoleteRun ? classifyWorkflowRunJournals(root, {
4090
+ currentBranch: session.branchName,
4091
+ currentHeads
4092
+ }) : classifiedRuns;
4093
+ const interruptedRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
3913
4094
  runId: journal.runId,
3914
4095
  command: journal.command,
3915
4096
  status: journal.status,
@@ -3919,7 +4100,7 @@ async function workflowRecover(helpers, input = {}) {
3919
4100
  failure: journal.failure,
3920
4101
  resumeCommand: `treeseed resume ${journal.runId}`
3921
4102
  }));
3922
- const staleRuns = classifiedRuns.filter((entry) => entry.classification.state === "stale").map(({ journal, classification }) => ({
4103
+ const staleRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "stale").map(({ journal, classification }) => ({
3923
4104
  runId: journal.runId,
3924
4105
  command: journal.command,
3925
4106
  status: journal.status,
@@ -3929,7 +4110,7 @@ async function workflowRecover(helpers, input = {}) {
3929
4110
  failure: journal.failure,
3930
4111
  classification
3931
4112
  }));
3932
- const obsoleteRuns = classifiedRuns.filter((entry) => entry.classification.state === "obsolete").map(({ journal, classification }) => ({
4113
+ const obsoleteRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "obsolete").map(({ journal, classification }) => ({
3933
4114
  runId: journal.runId,
3934
4115
  command: journal.command,
3935
4116
  status: journal.status,
@@ -3952,6 +4133,7 @@ async function workflowRecover(helpers, input = {}) {
3952
4133
  staleRuns,
3953
4134
  obsoleteRuns,
3954
4135
  prunedRuns,
4136
+ markedObsoleteRun,
3955
4137
  selectedRun,
3956
4138
  runCount: journals.length
3957
4139
  },
@@ -247,6 +247,16 @@ function isReleaseGateOnlyCompletion(journal) {
247
247
  const pendingStep = journal.steps.find((step) => step.status === "pending");
248
248
  return pendingStep?.id === "release-root-gates" || pendingStep?.id === "release-back-merge" || pendingStep?.id === "cleanup-dev-tags";
249
249
  }
250
+ function releaseStepData(journal, stepId) {
251
+ return stringRecord(journal.steps.find((step) => step.id === stepId)?.data);
252
+ }
253
+ function expectedPackageHeadAfterReleaseGate(journal, packageName) {
254
+ const data = releaseStepData(journal, `release-${packageName}`);
255
+ const backMerge = stringRecord(data?.backMerge);
256
+ if (typeof backMerge?.commitSha === "string") return backMerge.commitSha;
257
+ if (typeof data?.commitSha === "string") return data.commitSha;
258
+ return null;
259
+ }
250
260
  function classifyWorkflowRunJournal(journal, options = {}) {
251
261
  const reasons = [];
252
262
  const now = options.now ?? nowIso();
@@ -283,6 +293,22 @@ function classifyWorkflowRunJournal(journal, options = {}) {
283
293
  reasons.push(`current branch ${options.currentBranch} does not match journal branch ${journal.session.branchName}`);
284
294
  }
285
295
  const releaseGateOnlyCompletion = isReleaseGateOnlyCompletion(journal);
296
+ if (journal.command === "release" && options.currentHeads && releaseGateOnlyCompletion) {
297
+ const rootRelease = releaseStepData(journal, "release-root");
298
+ const expectedRootHead = typeof rootRelease?.stagingCommit === "string" ? rootRelease.stagingCommit : null;
299
+ const rootHead = options.currentHeads["@treeseed/market"];
300
+ if (rootHead && expectedRootHead && rootHead !== expectedRootHead) {
301
+ reasons.push(`market staging head changed from ${expectedRootHead} to ${rootHead}`);
302
+ }
303
+ const releasePlan = releaseStepData(journal, "release-plan");
304
+ for (const name of selectedReleasePackageNames(releasePlan)) {
305
+ const currentHead = options.currentHeads[name];
306
+ const expectedHead = expectedPackageHeadAfterReleaseGate(journal, name);
307
+ if (currentHead && expectedHead && currentHead !== expectedHead) {
308
+ reasons.push(`${name} staging head changed from ${expectedHead} to ${currentHead}`);
309
+ }
310
+ }
311
+ }
286
312
  if (journal.command === "release" && options.currentHeads && !releaseGateOnlyCompletion) {
287
313
  const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
288
314
  if (releasePlan) {
@@ -25,6 +25,7 @@ export type TreeseedWorkflowRecovery = {
25
25
  recoverCommand?: string | null;
26
26
  resumeCommand?: string | null;
27
27
  lock?: Record<string, unknown> | null;
28
+ localCleanup?: Record<string, unknown> | null;
28
29
  };
29
30
  export type TreeseedWorkflowExecutionMode = 'execute' | 'plan';
30
31
  export type TreeseedWorkflowWorktreeMode = 'auto' | 'on' | 'off';
@@ -214,6 +215,7 @@ export type TreeseedReleaseInput = {
214
215
  ciMode?: TreeseedWorkflowCiMode;
215
216
  worktreeMode?: TreeseedWorkflowWorktreeMode;
216
217
  workspaceLinks?: 'auto' | 'off';
218
+ fresh?: boolean;
217
219
  plan?: boolean;
218
220
  dryRun?: boolean;
219
221
  };
@@ -223,6 +225,8 @@ export type TreeseedResumeInput = {
223
225
  export type TreeseedRecoverInput = {
224
226
  runId?: string;
225
227
  pruneStale?: boolean;
228
+ obsoleteRunId?: string;
229
+ obsoleteReason?: string;
226
230
  };
227
231
  export type TreeseedDestroyInput = {
228
232
  target?: 'local' | 'staging' | 'prod';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.6.32",
3
+ "version": "0.6.33",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -184,11 +184,9 @@ jobs:
184
184
  set -euo pipefail
185
185
  npm run verify:local 2>&1 | tee verify.log
186
186
  if test -f ./packages/sdk/scripts/check-build-warnings.mjs; then
187
- node ./packages/sdk/scripts/check-build-warnings.mjs verify.log \
188
- --allow 'Module "url" has been externalized for browser compatibility, imported by ".*libsodium-sumo.*"'
187
+ node ./packages/sdk/scripts/check-build-warnings.mjs verify.log
189
188
  elif test -f ./node_modules/@treeseed/sdk/dist/scripts/check-build-warnings.js; then
190
- node ./node_modules/@treeseed/sdk/dist/scripts/check-build-warnings.js verify.log \
191
- --allow 'Module "url" has been externalized for browser compatibility, imported by ".*libsodium-sumo.*"'
189
+ node ./node_modules/@treeseed/sdk/dist/scripts/check-build-warnings.js verify.log
192
190
  else
193
191
  echo "Unable to resolve @treeseed/sdk warning scanner entrypoint."
194
192
  exit 1