@opengsd/gsd-pi 1.3.0-dev.72e3af2a → 1.3.0-dev.965d1788

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 (110) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
  4. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  5. package/dist/resources/extensions/gsd/auto-verification.js +5 -2
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +6 -0
  7. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
  8. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
  9. package/dist/resources/extensions/gsd/flat-phase-migration.js +45 -0
  10. package/dist/resources/extensions/gsd/git-service.js +1 -0
  11. package/dist/resources/extensions/gsd/gitignore.js +3 -0
  12. package/dist/resources/extensions/gsd/json-persistence.js +20 -0
  13. package/dist/resources/extensions/gsd/knowledge-backfill.js +18 -11
  14. package/dist/resources/extensions/gsd/memory-backfill.js +28 -21
  15. package/dist/resources/extensions/gsd/metrics.js +4 -4
  16. package/dist/resources/extensions/gsd/orphan-stash-audit.js +92 -16
  17. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  18. package/dist/resources/extensions/gsd/safety/file-change-validator.js +5 -2
  19. package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -2
  20. package/dist/resources/extensions/gsd/undo.js +43 -6
  21. package/dist/resources/extensions/gsd/verification-gate.js +7 -0
  22. package/dist/resources/extensions/gsd/workflow-manifest.js +4 -1
  23. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  24. package/dist/web/standalone/.next/BUILD_ID +1 -1
  25. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  26. package/dist/web/standalone/.next/build-manifest.json +2 -2
  27. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  28. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.html +1 -1
  45. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  52. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  54. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  55. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  56. package/package.json +1 -1
  57. package/packages/cloud-mcp-gateway/package.json +2 -2
  58. package/packages/contracts/package.json +1 -1
  59. package/packages/daemon/package.json +4 -4
  60. package/packages/gsd-agent-core/package.json +5 -5
  61. package/packages/gsd-agent-modes/package.json +7 -7
  62. package/packages/mcp-server/package.json +4 -4
  63. package/packages/native/package.json +1 -1
  64. package/packages/pi-agent-core/package.json +1 -1
  65. package/packages/pi-ai/package.json +1 -1
  66. package/packages/pi-coding-agent/package.json +7 -7
  67. package/packages/pi-tui/package.json +2 -2
  68. package/packages/rpc-client/package.json +2 -2
  69. package/pkg/package.json +1 -1
  70. package/src/resources/extensions/gsd/auto/session.ts +1 -0
  71. package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
  72. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
  73. package/src/resources/extensions/gsd/auto-verification.ts +5 -1
  74. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -0
  75. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
  76. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
  77. package/src/resources/extensions/gsd/flat-phase-migration.ts +44 -0
  78. package/src/resources/extensions/gsd/git-service.ts +1 -0
  79. package/src/resources/extensions/gsd/gitignore.ts +3 -0
  80. package/src/resources/extensions/gsd/json-persistence.ts +20 -0
  81. package/src/resources/extensions/gsd/knowledge-backfill.ts +16 -10
  82. package/src/resources/extensions/gsd/memory-backfill.ts +26 -20
  83. package/src/resources/extensions/gsd/metrics.ts +4 -4
  84. package/src/resources/extensions/gsd/orphan-stash-audit.ts +108 -20
  85. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  86. package/src/resources/extensions/gsd/safety/file-change-validator.ts +8 -2
  87. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +4 -3
  88. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +28 -0
  89. package/src/resources/extensions/gsd/tests/complete-slice-reopen-handoff.test.ts +69 -0
  90. package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +51 -0
  91. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +40 -0
  92. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +37 -2
  93. package/src/resources/extensions/gsd/tests/gitignore-bg-shell-runtime.test.ts +4 -0
  94. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +11 -2
  95. package/src/resources/extensions/gsd/tests/json-persistence-atomic.test.ts +47 -0
  96. package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +35 -0
  97. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +31 -1
  98. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +6 -0
  99. package/src/resources/extensions/gsd/tests/metrics.test.ts +1 -0
  100. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +91 -7
  101. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +10 -0
  102. package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
  103. package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
  104. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +54 -0
  105. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
  106. package/src/resources/extensions/gsd/undo.ts +47 -6
  107. package/src/resources/extensions/gsd/verification-gate.ts +8 -0
  108. package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
  109. /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → -ji77QJck2IKIwrGza1Bs}/_buildManifest.js +0 -0
  110. /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → -ji77QJck2IKIwrGza1Bs}/_ssgManifest.js +0 -0
@@ -11,6 +11,8 @@ import {
11
11
  verificationRetryKey,
12
12
  VERIFICATION_RETRY_MAX_DELAY_MS,
13
13
  } from "../auto/verification-retry-policy.ts";
14
+ import { formatFailureContext, formatFailureSignature } from "../verification-gate.ts";
15
+ import type { VerificationResult } from "../types.ts";
14
16
 
15
17
  test("verificationRetryDelayMs uses exponential backoff with cap", () => {
16
18
  assert.deepEqual(verificationRetryDelayMs(1, () => 0.5), {
@@ -31,9 +33,22 @@ test("verificationRetryDelayMs uses exponential backoff with cap", () => {
31
33
  });
32
34
  });
33
35
 
34
- test("decideVerificationRetry pauses on duplicate failure context", () => {
36
+ test("decideVerificationRetry pauses on repeated failure context across attempts", () => {
35
37
  const failureContext = "lint failed\n";
36
- const failureHash = hashVerificationFailureContext(failureContext);
38
+ const firstDecision = decideVerificationRetry({
39
+ unitType: "execute-task",
40
+ retryInfo: {
41
+ unitId: "M001/S01/T01",
42
+ failureContext,
43
+ attempt: 1,
44
+ },
45
+ previousFailureHash: undefined,
46
+ random: () => 0.5,
47
+ });
48
+
49
+ assert.equal(firstDecision.action, "delay");
50
+ assert.equal(firstDecision.failureHash, hashVerificationFailureContext(failureContext));
51
+
37
52
  const decision = decideVerificationRetry({
38
53
  unitType: "execute-task",
39
54
  retryInfo: {
@@ -41,7 +56,7 @@ test("decideVerificationRetry pauses on duplicate failure context", () => {
41
56
  failureContext,
42
57
  attempt: 2,
43
58
  },
44
- previousFailureHash: failureHash,
59
+ previousFailureHash: firstDecision.failureHash,
45
60
  random: () => 0.5,
46
61
  });
47
62
 
@@ -49,7 +64,7 @@ test("decideVerificationRetry pauses on duplicate failure context", () => {
49
64
  action: "pause",
50
65
  reason: "duplicate-failure-context",
51
66
  key: verificationRetryKey("execute-task", "M001/S01/T01"),
52
- failureHash,
67
+ failureHash: firstDecision.failureHash,
53
68
  });
54
69
  });
55
70
 
@@ -71,6 +86,56 @@ test("decideVerificationRetry delays changed failure context", () => {
71
86
  assert.equal(decision.baseDelayMs, 4_000);
72
87
  });
73
88
 
89
+ test("decideVerificationRetry pauses duplicate host-check failures with volatile stderr", () => {
90
+ const makeResult = (stderr: string): VerificationResult => ({
91
+ passed: false,
92
+ discoverySource: "package-json",
93
+ timestamp: Date.now(),
94
+ checks: [
95
+ {
96
+ command: "npm test",
97
+ exitCode: 1,
98
+ stdout: "",
99
+ stderr,
100
+ durationMs: 100,
101
+ },
102
+ ],
103
+ });
104
+ const first = makeResult("failed after 120ms in /tmp/gsd-a worker pid 101");
105
+ const second = makeResult("failed after 133ms in /tmp/gsd-b worker pid 202");
106
+ const firstContext = formatFailureContext(first);
107
+ const secondContext = formatFailureContext(second);
108
+
109
+ assert.notEqual(
110
+ hashVerificationFailureContext(firstContext),
111
+ hashVerificationFailureContext(secondContext),
112
+ );
113
+
114
+ const signature = formatFailureSignature(first);
115
+ assert.equal(signature, "npm test#1");
116
+ assert.equal(formatFailureSignature(second), signature);
117
+
118
+ const failureHash = hashVerificationFailureContext(signature);
119
+ const decision = decideVerificationRetry({
120
+ unitType: "execute-task",
121
+ retryInfo: {
122
+ unitId: "M001/S01/T01",
123
+ failureContext: secondContext,
124
+ signature: formatFailureSignature(second),
125
+ attempt: 2,
126
+ },
127
+ previousFailureHash: failureHash,
128
+ random: () => 0.5,
129
+ });
130
+
131
+ assert.deepEqual(decision, {
132
+ action: "pause",
133
+ reason: "duplicate-failure-context",
134
+ key: verificationRetryKey("execute-task", "M001/S01/T01"),
135
+ failureHash,
136
+ });
137
+ });
138
+
74
139
  test("decideVerificationRetry pauses when retry context is missing", () => {
75
140
  assert.deepEqual(
76
141
  decideVerificationRetry({
@@ -380,6 +380,60 @@ test('workflow-manifest: bootstrapFromManifest preserves target_repositories', (
380
380
  }
381
381
  });
382
382
 
383
+ test('workflow-manifest: bootstrapFromManifest does not truncate projections when artifact content is empty', () => {
384
+ const base = tempDir();
385
+ openDatabase(tempDbPath(base));
386
+ try {
387
+ insertMilestone({ id: 'M001', title: 'Projection Milestone' });
388
+ insertArtifact({
389
+ path: '.gsd/milestones/M001/M001-PLAN.md',
390
+ artifact_type: 'PLAN',
391
+ milestone_id: 'M001',
392
+ slice_id: null,
393
+ task_id: null,
394
+ full_content: '# Manifest Plan',
395
+ });
396
+ insertArtifact({
397
+ path: '.gsd/milestones/M001/M001-VALIDATION.md',
398
+ artifact_type: 'VALIDATION',
399
+ milestone_id: 'M001',
400
+ slice_id: null,
401
+ task_id: null,
402
+ full_content: '# Manifest Validation',
403
+ });
404
+ writeManifest(base);
405
+
406
+ const planPath = path.join(base, '.gsd', 'milestones', 'M001', 'M001-PLAN.md');
407
+ const validationPath = path.join(base, '.gsd', 'milestones', 'M001', 'M001-VALIDATION.md');
408
+ fs.mkdirSync(path.dirname(planPath), { recursive: true });
409
+ fs.writeFileSync(planPath, '# Existing Plan\n\nKeep this.');
410
+ fs.writeFileSync(validationPath, '# Existing Validation\n\nKeep this.');
411
+
412
+ const manifestPath = path.join(base, '.gsd', 'state-manifest.json');
413
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;
414
+ const artifacts = manifest['artifacts'] as Array<Record<string, unknown>>;
415
+ const emptyArtifact = artifacts.find((r) => r['path'] === '.gsd/milestones/M001/M001-PLAN.md');
416
+ const absentArtifact = artifacts.find((r) => r['path'] === '.gsd/milestones/M001/M001-VALIDATION.md');
417
+ assert.ok(emptyArtifact !== undefined);
418
+ assert.ok(absentArtifact !== undefined);
419
+ emptyArtifact['full_content'] = '';
420
+ delete absentArtifact['full_content'];
421
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
422
+ closeDatabase();
423
+
424
+ const newDbPath = path.join(base, 'new.db');
425
+ openDatabase(newDbPath);
426
+ const restored = bootstrapFromManifest(base);
427
+ assert.strictEqual(restored, true);
428
+
429
+ assert.strictEqual(fs.readFileSync(planPath, 'utf-8'), '# Existing Plan\n\nKeep this.');
430
+ assert.strictEqual(fs.readFileSync(validationPath, 'utf-8'), '# Existing Validation\n\nKeep this.');
431
+ } finally {
432
+ closeDatabase();
433
+ cleanupDir(base);
434
+ }
435
+ });
436
+
383
437
  test('workflow-manifest: bootstrapFromManifest preserves optional rows from old manifests', () => {
384
438
  const base = tempDir();
385
439
  openDatabase(tempDbPath(base));
@@ -23,7 +23,7 @@ import {
23
23
  } from "../gsd-db.js";
24
24
  import { gsdProjectionRoot, clearPathCache, relMilestoneFile } from "../paths.js";
25
25
  import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
26
- import { isClosedStatus } from "../status-guards.js";
26
+ import { isClosedStatus, isDeferredStatus } from "../status-guards.js";
27
27
  import { saveFile, clearParseCache } from "../files.js";
28
28
  import { invalidateStateCache } from "../state.js";
29
29
  import { stripIdPrefix } from "../workflow-projections.js";
@@ -180,7 +180,7 @@ export async function handleCompleteMilestone(
180
180
  return;
181
181
  }
182
182
 
183
- const incompleteSlices = slices.filter(s => !isClosedStatus(s.status));
183
+ const incompleteSlices = slices.filter(s => !isClosedStatus(s.status) && !isDeferredStatus(s.status));
184
184
  if (incompleteSlices.length > 0) {
185
185
  const incompleteIds = incompleteSlices.map(s => `${s.id} (status: ${s.status})`).join(", ");
186
186
  guardError = `incomplete slices: ${incompleteIds}`;
@@ -189,6 +189,7 @@ export async function handleCompleteMilestone(
189
189
 
190
190
  // Deep check: verify all tasks in all slices are complete
191
191
  for (const slice of slices) {
192
+ if (isDeferredStatus(slice.status)) continue;
192
193
  const tasks = getSliceTasks(params.milestoneId, slice.id);
193
194
  const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
194
195
  if (incompleteTasks.length > 0) {
@@ -15,6 +15,7 @@ import { gsdRoot, resolveTasksDir, resolveSlicePath, resolveSliceFile, resolveTa
15
15
  import { sendDesktopNotification } from "./notifications.js";
16
16
  import { getTask, getSlice, getSliceTasks, updateTaskStatus, resetSliceCascade } from "./gsd-db.js";
17
17
  import { renderPlanCheckboxes, renderRoadmapCheckboxes } from "./markdown-renderer.js";
18
+ import { UNIT_REGISTRY } from "./unit-registry.js";
18
19
 
19
20
  /**
20
21
  * Undo the last completed unit: revert git commits,
@@ -42,16 +43,20 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
42
43
  return;
43
44
  }
44
45
 
45
- // Extract unit type and ID from the most recent activity log filename
46
- // Format: <seq>-<unitType>-<unitId>.jsonl
47
- const match = files[0].match(/^\d+-(.+?)-(.+)\.jsonl$/);
48
- if (!match) {
46
+ // Extract unit type and ID from the most recent activity log filename.
47
+ // Both the unit type and the unit ID may contain hyphens, so anchor on the
48
+ // known unit-type vocabulary instead of guessing the unit-ID shape: a regex
49
+ // tuned to milestone-shaped IDs rejects project-level units whose IDs are
50
+ // symbolic (e.g. discuss-project uses PROJECT, workflow-preferences uses
51
+ // WORKFLOW-PREFS).
52
+ const parsed = parseActivityLogFilename(files[0]);
53
+ if (!parsed) {
49
54
  ctx.ui.notify("Nothing to undo — could not parse latest activity log.", "warning");
50
55
  return;
51
56
  }
52
57
 
53
- const unitType = match[1];
54
- const unitId = match[2].replace(/-/g, "/");
58
+ const unitType = parsed.unitType;
59
+ const unitId = parsed.unitId.replace(/-/g, "/");
55
60
 
56
61
  if (!force) {
57
62
  ctx.ui.notify(
@@ -100,6 +105,11 @@ export async function handleUndo(args: string, ctx: ExtensionCommandContext, _pi
100
105
  if (unitType === "execute-task" && task !== undefined && slice !== undefined) {
101
106
  const [mid, sid, tid] = [milestone, slice, task];
102
107
  planUpdated = uncheckTaskInPlan(basePath, mid, sid, tid);
108
+ if (getTask(mid, sid, tid)) {
109
+ updateTaskStatus(mid, sid, tid, "pending");
110
+ await renderPlanCheckboxes(basePath, mid, sid);
111
+ planUpdated = true;
112
+ }
103
113
  }
104
114
 
105
115
  // 3. Try to revert git commits from activity log
@@ -379,6 +389,37 @@ export async function handleResetSlice(
379
389
 
380
390
  // ─── Helpers ──────────────────────────────────────────────────────────────────
381
391
 
392
+ // Known unit types sorted longest-first so a more specific type (e.g.
393
+ // "execute-task-simple") matches before a prefix of it ("execute-task") when
394
+ // splitting "<seq>-<unitType>-<unitId>.jsonl".
395
+ const UNIT_TYPES_BY_LENGTH_DESC: readonly string[] = Object.keys(UNIT_REGISTRY).sort(
396
+ (a, b) => b.length - a.length,
397
+ );
398
+
399
+ /**
400
+ * Parse an activity-log filename of the form `<seq>-<unitType>-<unitId>.jsonl`
401
+ * (the format written by activity-log.ts). Both the unit type and the unit ID
402
+ * may contain hyphens, so we anchor on the known unit-type vocabulary rather
403
+ * than guessing the ID shape. This keeps non-milestone IDs (e.g. PROJECT,
404
+ * WORKFLOW-PREFS) parseable. Returns null when the name has no sequence prefix
405
+ * or does not start with a recognised unit type.
406
+ */
407
+ export function parseActivityLogFilename(
408
+ filename: string,
409
+ ): { unitType: string; unitId: string } | null {
410
+ const seqMatch = filename.match(/^\d+-(.+)\.jsonl$/);
411
+ if (!seqMatch) return null;
412
+ const rest = seqMatch[1];
413
+ for (const unitType of UNIT_TYPES_BY_LENGTH_DESC) {
414
+ const prefix = `${unitType}-`;
415
+ if (rest.startsWith(prefix)) {
416
+ const unitId = rest.slice(prefix.length);
417
+ if (unitId.length > 0) return { unitType, unitId };
418
+ }
419
+ }
420
+ return null;
421
+ }
422
+
382
423
  export function uncheckTaskInPlan(basePath: string, mid: string, sid: string, tid: string): boolean {
383
424
  const slicePath = resolveSlicePath(basePath, mid, sid);
384
425
  if (!slicePath) return false;
@@ -244,6 +244,14 @@ export function formatFailureContext(result: VerificationResult): string {
244
244
  return header + body;
245
245
  }
246
246
 
247
+ export function formatFailureSignature(result: VerificationResult): string {
248
+ return result.checks
249
+ .filter((check) => check.exitCode !== 0)
250
+ .map((check) => `${check.command.trim()}#${check.exitCode}`)
251
+ .sort()
252
+ .join("\n");
253
+ }
254
+
247
255
  // ─── Gate Execution ─────────────────────────────────────────────────────────
248
256
 
249
257
  /** Characters that indicate shell control syntax when unquoted in a command string. */
@@ -414,9 +414,12 @@ function restoreArtifactProjections(basePath: string, manifest: StateManifest):
414
414
  if (manifest.artifacts === undefined) return;
415
415
 
416
416
  for (const artifact of manifest.artifacts) {
417
+ const content = artifact.full_content ?? "";
418
+ if (content === "") continue;
419
+
417
420
  atomicWriteSync(
418
421
  artifactProjectionPath(basePath, artifact.path),
419
- artifact.full_content ?? "",
422
+ content,
420
423
  );
421
424
  }
422
425
  }