@deftai/directive-core 0.104.0 → 0.105.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 (225) hide show
  1. package/dist/authz/classify.js +526 -42
  2. package/dist/check/cached-orchestrator.d.ts +1 -1
  3. package/dist/check/cached-orchestrator.js +2 -2
  4. package/dist/check/named-cause.d.ts +1 -1
  5. package/dist/check/named-cause.js +33 -3
  6. package/dist/codebase/default-extractor.d.ts +5 -0
  7. package/dist/codebase/default-extractor.js +7 -15
  8. package/dist/deposit/copy-tree.js +60 -0
  9. package/dist/deposit/python-free.js +26 -18
  10. package/dist/doctor/checks.d.ts +8 -0
  11. package/dist/doctor/checks.js +37 -25
  12. package/dist/doctor/doctor-state.d.ts +4 -0
  13. package/dist/doctor/doctor-state.js +7 -1
  14. package/dist/doctor/main.js +9 -19
  15. package/dist/doctor/types.d.ts +9 -0
  16. package/dist/fs/contained-write.d.ts +115 -1
  17. package/dist/fs/contained-write.js +241 -5
  18. package/dist/fs/mutation-ledger.d.ts +54 -0
  19. package/dist/fs/mutation-ledger.js +133 -0
  20. package/dist/fs/non-product-dirs.d.ts +79 -0
  21. package/dist/fs/non-product-dirs.js +96 -0
  22. package/dist/hooks/dispatcher.d.ts +3 -1
  23. package/dist/hooks/dispatcher.js +14 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/init-deposit/agent-hooks.js +17 -15
  27. package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
  28. package/dist/init-deposit/core-guard-pin-content.js +393 -0
  29. package/dist/init-deposit/gitignore.js +9 -0
  30. package/dist/init-deposit/hygiene.d.ts +47 -11
  31. package/dist/init-deposit/hygiene.js +172 -37
  32. package/dist/init-deposit/legacy-detect.js +6 -2
  33. package/dist/init-deposit/refresh.d.ts +21 -1
  34. package/dist/init-deposit/refresh.js +106 -9
  35. package/dist/init-deposit/scaffold.js +28 -279
  36. package/dist/init-deposit/slash-deposit.js +13 -7
  37. package/dist/init-deposit/xbrief-projections.d.ts +8 -1
  38. package/dist/init-deposit/xbrief-projections.js +24 -11
  39. package/dist/intake/clause-derivation.d.ts +38 -1
  40. package/dist/intake/clause-derivation.js +211 -9
  41. package/dist/intake/issue-ingest.d.ts +3 -1
  42. package/dist/intake/issue-ingest.js +35 -6
  43. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
  44. package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
  45. package/dist/literal-acceptance/capture.d.ts +10 -0
  46. package/dist/literal-acceptance/capture.js +224 -75
  47. package/dist/literal-acceptance/evaluate.d.ts +29 -4
  48. package/dist/literal-acceptance/evaluate.js +70 -2
  49. package/dist/literal-acceptance/index.d.ts +3 -3
  50. package/dist/literal-acceptance/index.js +3 -3
  51. package/dist/literal-acceptance/safety.d.ts +41 -0
  52. package/dist/literal-acceptance/safety.js +126 -2
  53. package/dist/literal-acceptance/types.d.ts +8 -0
  54. package/dist/orphan-active/evaluate.d.ts +7 -1
  55. package/dist/orphan-active/evaluate.js +102 -23
  56. package/dist/platform/platform-capabilities.js +2 -3
  57. package/dist/policy/base-branch.d.ts +37 -0
  58. package/dist/policy/base-branch.js +114 -0
  59. package/dist/policy/branch-sync.d.ts +80 -0
  60. package/dist/policy/branch-sync.js +211 -0
  61. package/dist/policy/ceremony-dial-escalation.js +1 -0
  62. package/dist/policy/ceremony-dial.js +4 -14
  63. package/dist/policy/delivery-branch.d.ts +5 -3
  64. package/dist/policy/delivery-branch.js +7 -3
  65. package/dist/policy/file-size-thresholds.d.ts +24 -0
  66. package/dist/policy/file-size-thresholds.js +23 -0
  67. package/dist/policy/forge-outage-retry.d.ts +47 -0
  68. package/dist/policy/forge-outage-retry.js +157 -0
  69. package/dist/policy/index.d.ts +7 -0
  70. package/dist/policy/index.js +66 -7
  71. package/dist/policy/project-invariants.d.ts +50 -0
  72. package/dist/policy/project-invariants.js +205 -0
  73. package/dist/policy/sync-default.d.ts +129 -0
  74. package/dist/policy/sync-default.js +452 -0
  75. package/dist/policy/sync-max-files.d.ts +93 -0
  76. package/dist/policy/sync-max-files.js +192 -0
  77. package/dist/pr-monitor/absent-required.d.ts +11 -0
  78. package/dist/pr-monitor/absent-required.js +61 -0
  79. package/dist/pr-monitor/constants.d.ts +7 -0
  80. package/dist/pr-monitor/constants.js +7 -0
  81. package/dist/pr-monitor/index.d.ts +2 -1
  82. package/dist/pr-monitor/index.js +2 -1
  83. package/dist/pr-monitor/main.js +4 -0
  84. package/dist/pr-monitor/monitor.js +24 -1
  85. package/dist/pr-wait-mergeable/classify.js +6 -0
  86. package/dist/preflight/evaluate.d.ts +4 -0
  87. package/dist/preflight/evaluate.js +35 -1
  88. package/dist/preflight/index.d.ts +4 -0
  89. package/dist/preflight/index.js +2 -0
  90. package/dist/preflight/intended-placement.d.ts +62 -0
  91. package/dist/preflight/intended-placement.js +275 -0
  92. package/dist/preflight/project-invariants-gate.d.ts +29 -0
  93. package/dist/preflight/project-invariants-gate.js +82 -0
  94. package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
  95. package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
  96. package/dist/product-first-done-gate/acceptance.d.ts +7 -4
  97. package/dist/product-first-done-gate/acceptance.js +79 -8
  98. package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
  99. package/dist/product-first-done-gate/empty-resolution.js +12 -1
  100. package/dist/product-first-done-gate/evaluate.d.ts +12 -0
  101. package/dist/product-first-done-gate/evaluate.js +254 -31
  102. package/dist/product-first-done-gate/index.d.ts +2 -0
  103. package/dist/product-first-done-gate/index.js +1 -0
  104. package/dist/release/build-dist.d.ts +101 -0
  105. package/dist/release/build-dist.js +428 -46
  106. package/dist/run-summary/emit.d.ts +16 -10
  107. package/dist/run-summary/emit.js +26 -15
  108. package/dist/run-summary/index.d.ts +3 -2
  109. package/dist/run-summary/index.js +2 -1
  110. package/dist/run-summary/session-id.d.ts +21 -0
  111. package/dist/run-summary/session-id.js +92 -0
  112. package/dist/run-summary/types.d.ts +29 -4
  113. package/dist/run-summary/types.js +1 -0
  114. package/dist/scope/acceptance-evidence.d.ts +19 -7
  115. package/dist/scope/acceptance-evidence.js +38 -11
  116. package/dist/scope/delivery-evidence.d.ts +3 -3
  117. package/dist/scope/delivery-evidence.js +22 -27
  118. package/dist/scope/index.d.ts +1 -0
  119. package/dist/scope/index.js +1 -0
  120. package/dist/scope/parent-lineage.js +7 -6
  121. package/dist/scope/project-invariant-coverage.d.ts +54 -0
  122. package/dist/scope/project-invariant-coverage.js +322 -0
  123. package/dist/scope/transition.js +1 -1
  124. package/dist/scope-provenance/compare-intent.d.ts +27 -0
  125. package/dist/scope-provenance/compare-intent.js +233 -0
  126. package/dist/scope-provenance/digest.d.ts +18 -1
  127. package/dist/scope-provenance/digest.js +16 -11
  128. package/dist/scope-provenance/evaluate.d.ts +5 -1
  129. package/dist/scope-provenance/evaluate.js +64 -4
  130. package/dist/scope-provenance/extract-intent.d.ts +43 -0
  131. package/dist/scope-provenance/extract-intent.js +262 -0
  132. package/dist/scope-provenance/index.d.ts +7 -1
  133. package/dist/scope-provenance/index.js +7 -1
  134. package/dist/scope-provenance/intent-digest.d.ts +12 -0
  135. package/dist/scope-provenance/intent-digest.js +33 -0
  136. package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
  137. package/dist/scope-provenance/intent-evaluate.js +149 -0
  138. package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
  139. package/dist/scope-provenance/json-tokenizer.js +225 -0
  140. package/dist/scope-provenance/known-machine.d.ts +38 -0
  141. package/dist/scope-provenance/known-machine.js +157 -0
  142. package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
  143. package/dist/scope-provenance/mint-artifacts.js +425 -0
  144. package/dist/session/ac-pass-banking.d.ts +4 -36
  145. package/dist/session/ac-pass-banking.js +30 -77
  146. package/dist/session/ac-pass-reuse.d.ts +53 -0
  147. package/dist/session/ac-pass-reuse.js +95 -0
  148. package/dist/session/git.js +19 -4
  149. package/dist/session/index.d.ts +4 -0
  150. package/dist/session/index.js +4 -0
  151. package/dist/session/occupancy-stress-worker.d.ts +7 -0
  152. package/dist/session/occupancy-stress-worker.js +36 -0
  153. package/dist/session/occupancy.d.ts +97 -0
  154. package/dist/session/occupancy.js +422 -0
  155. package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
  156. package/dist/session/product-state-hash.d.ts +26 -0
  157. package/dist/session/product-state-hash.js +683 -0
  158. package/dist/session/session-ready.d.ts +2 -0
  159. package/dist/session/session-ready.js +52 -30
  160. package/dist/session/session-start.d.ts +7 -0
  161. package/dist/session/session-start.js +71 -7
  162. package/dist/session/verify-ac-session-cache.d.ts +42 -0
  163. package/dist/session/verify-ac-session-cache.js +126 -0
  164. package/dist/session/verify-session-ritual.d.ts +5 -0
  165. package/dist/session/verify-session-ritual.js +9 -0
  166. package/dist/slash/openclaw-deposit.js +13 -9
  167. package/dist/slice/constants.d.ts +2 -2
  168. package/dist/slice/constants.js +2 -2
  169. package/dist/slice/existing.js +9 -3
  170. package/dist/slice/lock.d.ts +16 -1
  171. package/dist/slice/lock.js +137 -11
  172. package/dist/story-ready/evaluate.d.ts +2 -0
  173. package/dist/story-ready/evaluate.js +13 -0
  174. package/dist/swarm/complete-cohort.d.ts +3 -0
  175. package/dist/swarm/complete-cohort.js +59 -11
  176. package/dist/swarm/finalize-cohort.js +33 -2
  177. package/dist/swarm/launch.d.ts +22 -0
  178. package/dist/swarm/launch.js +155 -0
  179. package/dist/swarm/routing-set-cli.d.ts +11 -1
  180. package/dist/swarm/routing-set-cli.js +13 -3
  181. package/dist/swarm/routing-verify.d.ts +12 -0
  182. package/dist/swarm/routing-verify.js +59 -20
  183. package/dist/swarm/routing.d.ts +14 -0
  184. package/dist/swarm/routing.js +53 -3
  185. package/dist/telemetry-coverage/fake-trial.js +10 -0
  186. package/dist/telemetry-coverage/kinds.js +2 -0
  187. package/dist/triage/actions/index.js +4 -1
  188. package/dist/ts-check-lane/index.d.ts +3 -0
  189. package/dist/ts-check-lane/index.js +2 -0
  190. package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
  191. package/dist/ts-check-lane/progress-reporter.js +38 -0
  192. package/dist/ts-check-lane/progress.d.ts +32 -0
  193. package/dist/ts-check-lane/progress.js +57 -0
  194. package/dist/ts-check-lane/run-lane.d.ts +3 -1
  195. package/dist/ts-check-lane/run-lane.js +8 -6
  196. package/dist/validate-content/validate-links.d.ts +8 -0
  197. package/dist/validate-content/validate-links.js +9 -15
  198. package/dist/value/feedback-file.js +12 -2
  199. package/dist/vbrief-build/index.d.ts +1 -0
  200. package/dist/vbrief-build/index.js +1 -0
  201. package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
  202. package/dist/vbrief-build/project-invariants-io.js +41 -0
  203. package/dist/vbrief-reconcile/graph.js +18 -1
  204. package/dist/vbrief-reconcile/index.d.ts +1 -1
  205. package/dist/vbrief-reconcile/index.js +1 -1
  206. package/dist/vbrief-reconcile/types.d.ts +13 -0
  207. package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
  208. package/dist/vbrief-reconcile/umbrellas.js +261 -26
  209. package/dist/vbrief-validate/index.d.ts +1 -1
  210. package/dist/vbrief-validate/index.js +1 -1
  211. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  212. package/dist/vbrief-validate/plan-hooks.js +68 -0
  213. package/dist/verify-ac/evaluate.d.ts +7 -1
  214. package/dist/verify-ac/evaluate.js +34 -26
  215. package/dist/verify-ac/flag.d.ts +4 -0
  216. package/dist/verify-ac/flag.js +16 -0
  217. package/dist/verify-ac/index.d.ts +2 -2
  218. package/dist/verify-ac/index.js +2 -2
  219. package/dist/verify-source/contained-writes.js +2 -0
  220. package/dist/verify-source/verify-stubs.d.ts +15 -1
  221. package/dist/verify-source/verify-stubs.js +20 -16
  222. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
  223. package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
  224. package/dist/xbrief-migrate/migrate-project.js +2 -5
  225. package/package.json +3 -3
@@ -8,7 +8,7 @@
8
8
  * Refs #1942, #1430, #1671.
9
9
  */
10
10
  import { execFileSync } from "node:child_process";
11
- import { existsSync, readFileSync, renameSync, statSync } from "node:fs";
11
+ import { existsSync, readFileSync, statSync } from "node:fs";
12
12
  import { platform as osPlatform } from "node:os";
13
13
  import { join, resolve } from "node:path";
14
14
  import { assertDepositContained } from "../deposit/contain.js";
@@ -18,6 +18,8 @@ import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
18
18
  import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
19
19
  import { readCorePackageVersion } from "../engine-version.js";
20
20
  import { readLiveGeneration, stampLiveGeneration } from "../freshness/generation.js";
21
+ import { containedRename } from "../fs/contained-write.js";
22
+ import { activeMutationLedger, formatMutationSummary, mutationSummaryJson, runInPortRecordMode, runWithMutationLedger, snapshotMutationSummary, } from "../fs/mutation-ledger.js";
21
23
  import { resolveLifecycleRoot } from "../layout/resolve.js";
22
24
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
23
25
  import { runOrgForceOnMigration } from "../policy/org-force-on-migration.js";
@@ -189,7 +191,11 @@ function migrateLegacyInstallManifest(projectDir, canonicalManifestPath) {
189
191
  const canonicalVersion = manifestTagToVersion(parseInstallManifest(readFileSync(canonical, "utf8")));
190
192
  if (legacyVersion !== null && legacyVersion === canonicalVersion)
191
193
  return;
192
- renameSync(legacy, join(projectDir, ".deft", "VERSION.premigrate"));
194
+ containedRename({
195
+ root: projectDir,
196
+ from: legacy,
197
+ to: join(projectDir, ".deft", "VERSION.premigrate"),
198
+ });
193
199
  }
194
200
  catch {
195
201
  // best-effort
@@ -312,8 +318,8 @@ export function printRefreshSideEffects(io, effects) {
312
318
  }
313
319
  io.printf("\nAGENTS.md refresh side effects (#1671): the refresh and framework payload swap\n");
314
320
  io.printf("left these framework files with uncommitted changes -- they belong in the\n");
315
- io.printf("framework deposit commit (the installer stages them before printing the\n");
316
- io.printf("`git add` list below, so there are no post-stage stragglers):\n");
321
+ io.printf("framework deposit commit (the installer stages this-run installer-managed\n");
322
+ io.printf("paths; ledger remainder stays unstaged):\n");
317
323
  for (const file of effects.files) {
318
324
  io.printf(` ${file}\n`);
319
325
  }
@@ -335,6 +341,32 @@ export function buildVersionSkewNotice(engineVersion, contentVersion, previousDe
335
341
  }
336
342
  return null;
337
343
  }
344
+ function displayVersion(version) {
345
+ return version === null || version.trim() === "" ? "unknown" : normalizeVersion(version);
346
+ }
347
+ /** True when recorded deposit version and content package version disagree (#3437). */
348
+ export function depositRefreshPending(previousDepositVersion, contentVersion) {
349
+ if (previousDepositVersion === null || previousDepositVersion.trim() === "")
350
+ return true;
351
+ return normalizeVersion(previousDepositVersion) !== normalizeVersion(contentVersion);
352
+ }
353
+ /** First-line dry-run headline when deposit manifest and content disagree (#3437). */
354
+ export function buildDepositVersionSkewHeadline(previousDepositVersion, contentVersion) {
355
+ if (!depositRefreshPending(previousDepositVersion, contentVersion))
356
+ return null;
357
+ return (`[deft update] Version skew: deposit manifest ${displayVersion(previousDepositVersion)}` +
358
+ ` -> content ${displayVersion(contentVersion)}`);
359
+ }
360
+ async function readDryRunVersions(projectDir, seams = {}) {
361
+ const deftDir = join(resolve(projectDir), CANONICAL_INSTALL_ROOT);
362
+ const resolveContent = seams.resolveContentRoot ?? resolveInstalledContentRoot;
363
+ const readPackageVersion = seams.readPackageVersion ?? readCorePackageVersion;
364
+ const contentRoot = await resolveContent();
365
+ return {
366
+ previousVersion: readRecordedDepositVersion(deftDir),
367
+ contentVersion: readContentPackageVersion(contentRoot, readPackageVersion),
368
+ };
369
+ }
338
370
  export function buildUpdateSummaryJson(input) {
339
371
  const { result, options, updateState, readiness } = input;
340
372
  return {
@@ -363,6 +395,8 @@ export function buildUpdateSummaryJson(input) {
363
395
  dirty_tree: false,
364
396
  dirty_files: [],
365
397
  staged_paths: result.stagedPaths,
398
+ mutations: mutationSummaryJson(result.mutations),
399
+ prettier_sensitive_rewrites: prettierSensitiveRewrites(result.mutations),
366
400
  backup_path: "",
367
401
  previous_version: result.previousDepositVersion ?? "",
368
402
  content_version: result.contentVersion,
@@ -370,6 +404,19 @@ export function buildUpdateSummaryJson(input) {
370
404
  agents_md_updated: result.agentsMdUpdated,
371
405
  };
372
406
  }
407
+ /** Consumer-owned prefixes whose rewrite can fail a repo Prettier/fmt gate (#3395). */
408
+ export const PRETTIER_SENSITIVE_CONSUMER_PREFIXES = ["xbrief/schemas/"];
409
+ export const PRETTIER_SENSITIVE_FMT_HINT = "run `task fmt` or your repo formatter before the upgrade PR";
410
+ /** Ledger `wrote` paths under prettier-sensitive consumer-owned prefixes. */
411
+ export function prettierSensitiveRewrites(summary) {
412
+ return summary.wrote.filter((path) => PRETTIER_SENSITIVE_CONSUMER_PREFIXES.some((prefix) => path === prefix.slice(0, -1) || path.startsWith(prefix)));
413
+ }
414
+ export function formatPrettierSensitiveAnnounce(paths) {
415
+ if (paths.length === 0)
416
+ return "";
417
+ return (`Rewritten consumer-owned paths (${PRETTIER_SENSITIVE_FMT_HINT}):\n` +
418
+ paths.map((path) => ` ${path}\n`).join(""));
419
+ }
373
420
  export function printUpdateComplete(result, io, updateState) {
374
421
  io.printf(result.alreadyCurrent
375
422
  ? "\nOK Deft framework payload already current.\n\n"
@@ -384,11 +431,22 @@ export function printUpdateComplete(result, io, updateState) {
384
431
  if (result.versionSkewNotice) {
385
432
  io.printf(`\n${result.versionSkewNotice}\n`);
386
433
  }
434
+ const mutationText = formatMutationSummary(result.mutations);
435
+ if (mutationText.length > 0) {
436
+ io.printf(`\n${mutationText}`);
437
+ }
438
+ const prettierText = formatPrettierSensitiveAnnounce(prettierSensitiveRewrites(result.mutations));
439
+ if (prettierText.length > 0) {
440
+ io.printf(`\n${prettierText}`);
441
+ }
387
442
  printMigrateNudgeIfNeeded(result.projectDir, io);
388
443
  io.printf("\n");
389
444
  }
390
445
  export async function runRefreshDeposit(args, io, seams = {}) {
391
446
  const projectDir = resolve(args.projectDir);
447
+ if (activeMutationLedger() === undefined) {
448
+ return runWithMutationLedger(projectDir, () => runRefreshDeposit(args, io, seams));
449
+ }
392
450
  const deftDir = join(projectDir, CANONICAL_INSTALL_ROOT);
393
451
  // #1912: refuse a legacy on-disk layout BEFORE any refresh. The npm CLI never
394
452
  // migrates -- the frozen Go bridge does (stage 1), then the npm path (stage 2).
@@ -545,6 +603,7 @@ export async function runRefreshDeposit(args, io, seams = {}) {
545
603
  const stagedResult = depositStagePaths(projectDir, {
546
604
  includeTaskfile: taskfileWired,
547
605
  includeCore: !alreadyCurrent,
606
+ printf: (text) => io.printf(text),
548
607
  });
549
608
  stagedPaths = stagedResult.stagedPaths;
550
609
  if (!alreadyCurrent) {
@@ -552,7 +611,7 @@ export async function runRefreshDeposit(args, io, seams = {}) {
552
611
  }
553
612
  else if (stagedResult.stagedPaths.length > 0) {
554
613
  io.printf("\nUpdated installer-managed projections for the current framework deposit:\n");
555
- io.printf(` git add ${stagedResult.stagedPaths.join(" ")}\n`);
614
+ io.printf(` git add -- ${stagedResult.stagedPaths.join(" ")}\n`);
556
615
  }
557
616
  }
558
617
  printRefreshSideEffects(io, { ...effects, payloadSwapped: !alreadyCurrent });
@@ -572,6 +631,7 @@ export async function runRefreshDeposit(args, io, seams = {}) {
572
631
  legacyLayout: false,
573
632
  taskfileWired,
574
633
  stagedPaths,
634
+ mutations: snapshotMutationSummary(),
575
635
  };
576
636
  }
577
637
  function buildRefusalJson(state, projectDir, message, command) {
@@ -602,10 +662,24 @@ function emitMigrationRequired(options, io, projectDir, classification) {
602
662
  }
603
663
  return UPDATE_REFUSED_EXIT_CODE;
604
664
  }
605
- /** Emit the classified plan for `--dry-run`/`--plan` without executing the refresh. */
606
- function emitDryRunPlan(options, io, projectDir, classification) {
665
+ /**
666
+ * Dest effects the port cannot yet capture (ADR-004 condition 3).
667
+ * Shrink as sites fold; never drop the printed label.
668
+ */
669
+ export const UPDATE_DRY_RUN_EXCLUSIONS = ["openclaw-home-skills"];
670
+ export const UPDATE_DRY_RUN_EXCLUSIONS_LABEL = "Excluded from dest plan (not yet captured by the port): OpenClaw $HOME skills.";
671
+ /** Emit the classified plan plus recorded port dest mutations (ADR-004). */
672
+ async function emitDryRunPlan(options, io, projectDir, classification) {
673
+ const { previousVersion, contentVersion } = await readDryRunVersions(projectDir, options.seams ?? {});
674
+ const refreshPending = depositRefreshPending(previousVersion, contentVersion);
675
+ const skewHeadline = buildDepositVersionSkewHeadline(previousVersion, contentVersion);
607
676
  const { state, plan: resolutionPlan } = classification;
608
- io.printf(`\n[deft update] dry-run -- classified plan (no changes written):\n`);
677
+ const silentIo = { printf: () => undefined };
678
+ const result = await runInPortRecordMode(() => runRefreshDeposit(options, silentIo, options.seams ?? {}));
679
+ if (skewHeadline) {
680
+ io.printf(`${skewHeadline}\n`);
681
+ }
682
+ io.printf(`\n[deft update] dry-run -- dest plan (recorded port calls, no changes written):\n`);
609
683
  io.printf(` State : ${state}\n`);
610
684
  io.printf(` Mode : ${resolutionPlan.mode}\n`);
611
685
  io.printf(` Root cause : ${resolutionPlan.nextAction.rootCause}\n`);
@@ -613,6 +687,14 @@ function emitDryRunPlan(options, io, projectDir, classification) {
613
687
  for (const warning of resolutionPlan.warnings) {
614
688
  io.printf(` Warning : ${warning}\n`);
615
689
  }
690
+ const mutationText = formatMutationSummary(result.mutations);
691
+ if (mutationText.length > 0) {
692
+ io.printf(`\n${mutationText}`);
693
+ }
694
+ else {
695
+ io.printf(`\nNo dest mutations recorded.\n`);
696
+ }
697
+ io.printf(`\n${UPDATE_DRY_RUN_EXCLUSIONS_LABEL}\n`);
616
698
  if (options.jsonOut) {
617
699
  options.writeOut(`${JSON.stringify({
618
700
  success: true,
@@ -621,8 +703,13 @@ function emitDryRunPlan(options, io, projectDir, classification) {
621
703
  update_state: state,
622
704
  mode: resolutionPlan.mode,
623
705
  project_dir: projectDir,
706
+ previous_version: previousVersion ?? "",
707
+ content_version: contentVersion,
708
+ deposit_refresh_pending: refreshPending,
624
709
  next_action: resolutionPlan.nextAction,
625
710
  warnings: resolutionPlan.warnings,
711
+ mutations: mutationSummaryJson(result.mutations),
712
+ exclusions: [...UPDATE_DRY_RUN_EXCLUSIONS],
626
713
  }, null, 2)}\n`);
627
714
  }
628
715
  return 0;
@@ -678,7 +765,17 @@ export async function runRefreshDepositCli(options) {
678
765
  return emitMigrationRequired(options, io, projectDir, classification);
679
766
  }
680
767
  if (options.dryRun) {
681
- return emitDryRunPlan(options, io, projectDir, classification);
768
+ try {
769
+ return await emitDryRunPlan(options, io, projectDir, classification);
770
+ }
771
+ catch (cause) {
772
+ const message = cause instanceof Error ? cause.message : String(cause);
773
+ options.writeErr(`directive update: ${message}\n`);
774
+ if (options.jsonOut) {
775
+ options.writeOut(`${JSON.stringify({ success: false, error: message, error_code: "refresh_deposit_failed" }, null, 2)}\n`);
776
+ }
777
+ return 1;
778
+ }
682
779
  }
683
780
  // #2266 a3: self-heal a mismatched / unreachable engine via the keystone
684
781
  // global-first ladder before the refresh proceeds.
@@ -5,15 +5,17 @@
5
5
  * Refs #1942, #1430, #1463, #1179.
6
6
  */
7
7
  import { execFileSync } from "node:child_process";
8
- import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
9
- import { mkdir, readdir, rm, stat } from "node:fs/promises";
8
+ import { existsSync, readFileSync, statSync } from "node:fs";
9
+ import { readdir, stat } from "node:fs/promises";
10
10
  import { platform } from "node:os";
11
11
  import { join, relative, resolve } from "node:path";
12
- import { containedWrite } from "../fs/contained-write.js";
12
+ import { containedChmod, containedDestExec, containedMkdir, containedRemove, containedWrite, } from "../fs/contained-write.js";
13
13
  import { assertDestinationNotSymlink, ProjectionContainmentError, } from "../fs/projection-containment.js";
14
14
  import { agentsRefreshPlan } from "../platform/agents-md.js";
15
+ import { renderCoreGuardBranchSyncIfBlock } from "../policy/branch-sync.js";
15
16
  import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
16
17
  import { CANONICAL_INSTALL_ROOT } from "./constants.js";
18
+ import { CORE_GUARD_PIN_CONTENT_PYTHON } from "./core-guard-pin-content.js";
17
19
  import { assertInstallerAllowlistHonors1430, installerManagedGuardErePatterns } from "./hygiene.js";
18
20
  import { writeAgentsSkillsFromInventory } from "./skill-discovery-deposit.js";
19
21
  import { syncConsumerXbriefSchemas } from "./xbrief-projections.js";
@@ -204,7 +206,7 @@ export function writeAgentsMd(projectDir, deftDir, io) {
204
206
  async function ensureVbriefLifecycleDirs(projectDir) {
205
207
  for (const sub of VBRIEF_LIFECYCLE_DIRS) {
206
208
  const dir = projectionTarget(projectDir, MIGRATED_ARTIFACT_DIR, sub);
207
- await mkdir(dir, { recursive: true, mode: 0o755 });
209
+ containedMkdir({ root: projectDir, target: dir });
208
210
  const gitkeep = join(dir, ".gitkeep");
209
211
  try {
210
212
  await stat(gitkeep);
@@ -243,11 +245,11 @@ export async function writeConsumerVbrief(projectDir, deftDir, io) {
243
245
  : "vbrief/ already present at project root — skipping.\n");
244
246
  return schemasChanged;
245
247
  }
246
- mkdirSync(consumerVbrief, { recursive: true });
248
+ containedMkdir({ root: projectDir, target: consumerVbrief });
247
249
  if (!vbriefMdPresent) {
248
250
  const fwVbriefMd = join(deftDir, "vbrief", "vbrief.md");
249
251
  if (existsSync(fwVbriefMd)) {
250
- copyFileSync(fwVbriefMd, vbriefMdDst);
252
+ containedProjectWrite(projectDir, vbriefMdDst, readFileSync(fwVbriefMd));
251
253
  }
252
254
  else {
253
255
  containedProjectWrite(projectDir, vbriefMdDst, VBRIEF_README_BODY);
@@ -373,7 +375,7 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
373
375
  try {
374
376
  const mode = statSync(dst).mode & 0o777;
375
377
  if ((mode & 0o111) === 0) {
376
- chmodSync(dst, HOOK_FILE_MODE);
378
+ containedChmod({ root: projectDir, target: dst, mode: HOOK_FILE_MODE });
377
379
  filesDeposited = true;
378
380
  }
379
381
  }
@@ -394,15 +396,12 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
394
396
  }
395
397
  });
396
398
  const setHooksPath = seams.setHooksPath ??
397
- ((dir, value) => {
398
- try {
399
- execFileSync("git", ["-C", dir, "config", "core.hooksPath", value], { encoding: "utf8" });
400
- return true;
401
- }
402
- catch {
403
- return false;
404
- }
405
- });
399
+ ((dir, value) => containedDestExec({
400
+ root: dir,
401
+ destTarget: join(".git", "config"),
402
+ file: "git",
403
+ args: ["-C", dir, "config", "core.hooksPath", value],
404
+ }).ok);
406
405
  const target = ".githooks";
407
406
  const current = getHooksPath(projectDir) ?? "";
408
407
  let configWired = false;
@@ -550,274 +549,20 @@ export function assertCoreGuardWorkflowLoadable(content) {
550
549
  }
551
550
  /**
552
551
  * Embedded python3 content check for package.json / lockfiles when co-travelling
553
- * with .deft/core/** (#3193). Mirrors TS `isUpgradePinPathContentAllowed`.
552
+ * with .deft/core/** (#3193 / #3427). Mirrors TS `isUpgradePinPathContentAllowed`.
554
553
  * Uses a single-quoted heredoc so GHA does not interpolate shell variables
555
- * inside the Python source (BASE/HEAD are argv).
554
+ * inside the Python source (BASE/HEAD are argv). Body is
555
+ * {@link CORE_GUARD_PIN_CONTENT_PYTHON} (twin of the Go embed).
556
556
  *
557
557
  * Every emitted line is indented to {@link CORE_GUARD_RUN_INDENT} so the YAML
558
558
  * `run: |` block stays intact (#3345). After GHA strips the common indent, the
559
559
  * shell heredoc body reaches python3 with correct relative indentation.
560
560
  */
561
561
  function coreGuardPinContentPython() {
562
- // Keep this compact: deposited into every consumer workflow on init/update.
563
- const pyBody = [
564
- "import json, re, subprocess, sys",
565
- "base_sha, head_sha = sys.argv[1], sys.argv[2]",
566
- "def git_show(sha, path):",
567
- " try:",
568
- " return subprocess.check_output(['git','show',f'{sha}:{path}'], text=True, stderr=subprocess.DEVNULL)",
569
- " except subprocess.CalledProcessError:",
570
- " return ''",
571
- "def changed_names():",
572
- " out = subprocess.check_output(['git','diff','--name-only',base_sha,head_sha], text=True)",
573
- " return [p for p in out.splitlines() if p]",
574
- "def is_dir_key(n):",
575
- " return n.startswith('@deftai/directive')",
576
- "DEP_FIELDS = ('dependencies','devDependencies','optionalDependencies','peerDependencies')",
577
- "def deep_eq(a,b):",
578
- " if a is b: return True",
579
- " if type(a) is not type(b): return False",
580
- " if isinstance(a, dict):",
581
- " if set(a) != set(b): return False",
582
- " return all(deep_eq(a[k], b[k]) for k in a)",
583
- " if isinstance(a, list):",
584
- " return len(a)==len(b) and all(deep_eq(x,y) for x,y in zip(a,b))",
585
- " return a == b",
586
- "def strip_pins(obj):",
587
- " if not isinstance(obj, dict): return obj",
588
- " out = {}",
589
- " for k,v in obj.items():",
590
- " if k in DEP_FIELDS and isinstance(v, dict):",
591
- " out[k] = {dk:dv for dk,dv in v.items() if not is_dir_key(dk)}",
592
- " else:",
593
- " out[k] = v",
594
- " return out",
595
- "def pkg_pin_only(base, head):",
596
- " try:",
597
- " b, h = json.loads(base or '{}'), json.loads(head or '{}')",
598
- " except json.JSONDecodeError:",
599
- " return False",
600
- " return deep_eq(strip_pins(b), strip_pins(h))",
601
- "def collect_deps(pkg):",
602
- " out = {}",
603
- " if not isinstance(pkg, dict): return out",
604
- " for f in DEP_FIELDS:",
605
- " block = pkg.get(f)",
606
- " if isinstance(block, dict):",
607
- " for k,v in block.items():",
608
- " if isinstance(v, str): out[k]=v",
609
- " elif isinstance(v, dict) and isinstance(v.get('version'), str): out[k]=v['version']",
610
- " elif v is not None: out[k]=str(v)",
611
- " return out",
612
- "def only_dir_diff(bd, hd):",
613
- " keys = set(bd)|set(hd)",
614
- " for k in keys:",
615
- " if is_dir_key(k): continue",
616
- " if bd.get(k) != hd.get(k): return False",
617
- " return True",
618
- "def npm_root_deps(lock):",
619
- " pkgs = lock.get('packages') if isinstance(lock, dict) else None",
620
- " if isinstance(pkgs, dict) and isinstance(pkgs.get(''), dict):",
621
- " return collect_deps(pkgs[''])",
622
- " deps = lock.get('dependencies') if isinstance(lock, dict) else None",
623
- " out = {}",
624
- " if isinstance(deps, dict):",
625
- " for k,v in deps.items():",
626
- " if isinstance(v, dict): out[k]=str(v.get('version', v))",
627
- " elif isinstance(v, str): out[k]=v",
628
- " return out",
629
- "def npm_lock_ok(base, head):",
630
- " try:",
631
- " b, h = json.loads(base or '{}'), json.loads(head or '{}')",
632
- " except json.JSONDecodeError:",
633
- " return False",
634
- " br, hr = npm_root_deps(b), npm_root_deps(h)",
635
- " if not only_dir_diff(br, hr): return False",
636
- " bp, hp = b.get('packages') or {}, h.get('packages') or {}",
637
- " if not isinstance(bp, dict): bp = {}",
638
- " if not isinstance(hp, dict): hp = {}",
639
- " all_keys = set(bp)|set(hp)",
640
- " for key in all_keys:",
641
- " if key == '': continue",
642
- " if 'node_modules/@deftai/directive' in key or '/@deftai/directive/' in key: continue",
643
- " if not deep_eq(bp.get(key), hp.get(key)): return False",
644
- " if not bp and not hp:",
645
- " def strip_dir(v):",
646
- " if not isinstance(v, dict): return v",
647
- " out = {}",
648
- " for k, val in v.items():",
649
- " if is_dir_key(k): continue",
650
- " if isinstance(val, dict):",
651
- " nxt = dict(val)",
652
- " if 'dependencies' in val: nxt['dependencies'] = strip_dir(val['dependencies'])",
653
- " out[k] = nxt",
654
- " else: out[k] = val",
655
- " return out",
656
- " if not deep_eq(strip_dir(b.get('dependencies')), strip_dir(h.get('dependencies'))): return False",
657
- " return True",
658
- "def pnpm_root_deps(raw):",
659
- " out = {}",
660
- " in_imp = in_root = in_dep = False",
661
- " cur = None",
662
- " def unq(s):",
663
- " s=s.strip()",
664
- ' if len(s)>=2 and s[0]==s[-1] and s[0] in "\'\\"": return s[1:-1]',
665
- " return s",
666
- " for line in raw.splitlines():",
667
- " if re.match(r'^importers:\\s*$', line):",
668
- " in_imp, in_root, in_dep, cur = True, False, False, None",
669
- " continue",
670
- " if not in_imp: continue",
671
- " if re.match(r'^[^\\s#]', line) and not line.startswith('importers'):",
672
- " break",
673
- ' if re.match(r"^ {2}(?:\\.|\'\\.\'|\\"\\.\\"):\\s*$", line):',
674
- " in_root, in_dep, cur = True, False, None",
675
- " continue",
676
- " if in_root and re.match(r'^ {2}\\S', line) and not re.match(r'^ {2}\\.', line):",
677
- " in_root, in_dep, cur = False, False, None",
678
- " continue",
679
- " if not in_root: continue",
680
- " if re.match(r'^ {4}(?:dependencies|devDependencies|optionalDependencies|peerDependencies):\\s*$', line):",
681
- " in_dep, cur = True, None",
682
- " continue",
683
- " if in_dep and re.match(r'^ {4}\\S', line):",
684
- " in_dep, cur = False, None",
685
- " continue",
686
- " if not in_dep: continue",
687
- " m = re.match(r'^ {6}(.+?):\\s*$', line)",
688
- " if m:",
689
- " cur = unq(m.group(1)); continue",
690
- " if cur:",
691
- " vm = re.match(r'^ {8}version:\\s*(.+?)\\s*$', line)",
692
- " if vm:",
693
- " out[cur] = unq(vm.group(1)); cur = None",
694
- " return out",
695
- "def pnpm_packages_by_name(raw):",
696
- " blocks, out = {}, {}",
697
- " in_pkg, cur, buf = False, None, []",
698
- " def flush():",
699
- " nonlocal cur, buf",
700
- " if cur is None: return",
701
- " blocks.setdefault(cur, []).append('\\n'.join(buf))",
702
- " cur, buf = None, []",
703
- " def unq(s):",
704
- " s=s.strip()",
705
- ' if len(s)>=2 and s[0]==s[-1] and s[0] in "\'\\"": return s[1:-1]',
706
- " return s",
707
- " for line in raw.splitlines():",
708
- " if re.match(r'^packages:\\s*$', line):",
709
- " flush(); in_pkg = True; continue",
710
- " if not in_pkg: continue",
711
- " if re.match(r'^[^\\s#]', line) and not line.startswith('packages'):",
712
- " flush(); break",
713
- " m = re.match(r'^ {2}(.+?):\\s*$', line)",
714
- " if m:",
715
- " flush()",
716
- " key = unq(m.group(1))",
717
- " at = key.find('@', 1) if key.startswith('@') else key.find('@')",
718
- " cur = key[:at] if at > 0 else key",
719
- " buf = [line]; continue",
720
- " if cur is not None: buf.append(line)",
721
- " flush()",
722
- " for name, blist in blocks.items():",
723
- " out[name] = '\\n---\\n'.join(sorted(blist))",
724
- " return out",
725
- "def pnpm_section_by_name(raw, section):",
726
- " blocks, out = {}, {}",
727
- " in_sec, cur, buf = False, None, []",
728
- " sec_re = re.compile(r'^' + re.escape(section) + r':\\s*$')",
729
- " def flush():",
730
- " nonlocal cur, buf",
731
- " if cur is None: return",
732
- " blocks.setdefault(cur, []).append('\\n'.join(buf))",
733
- " cur, buf = None, []",
734
- " def unq(s):",
735
- " s=s.strip()",
736
- ' if len(s)>=2 and s[0]==s[-1] and s[0] in "\'\\"": return s[1:-1]',
737
- " return s",
738
- " for line in raw.splitlines():",
739
- " if sec_re.match(line):",
740
- " flush(); in_sec = True; continue",
741
- " if not in_sec: continue",
742
- " if re.match(r'^[^\\s#]', line) and not line.startswith(section):",
743
- " flush(); break",
744
- " m = re.match(r'^ {2}(.+?):\\s*$', line)",
745
- " if m:",
746
- " flush()",
747
- " key = unq(m.group(1))",
748
- " at = key.find('@', 1) if key.startswith('@') else key.find('@')",
749
- " cur = key[:at] if at > 0 else key",
750
- " buf = [line]; continue",
751
- " if cur is not None: buf.append(line)",
752
- " flush()",
753
- " for name, blist in blocks.items():",
754
- " out[name] = '\\n---\\n'.join(sorted(blist))",
755
- " return out",
756
- "def pnpm_ok(base, head):",
757
- " br, hr = pnpm_root_deps(base), pnpm_root_deps(head)",
758
- " if not only_dir_diff(br, hr): return False",
759
- " for section in ('packages', 'snapshots'):",
760
- " bp, hp = pnpm_section_by_name(base, section), pnpm_section_by_name(head, section)",
761
- " for name in set(bp)|set(hp):",
762
- " if is_dir_key(name): continue",
763
- " if bp.get(name) != hp.get(name): return False",
764
- " return True",
765
- "def yarn_blocks(raw):",
766
- " blocks = {}",
767
- " names, buf = [], []",
768
- " def flush():",
769
- " nonlocal names, buf",
770
- " if not names: return",
771
- " body = '\\n'.join(buf)",
772
- " for n in names: blocks[n] = body",
773
- " names, buf = [], []",
774
- " for line in raw.splitlines():",
775
- " if line == '' or line.startswith('#'):",
776
- " if names and line == '': flush()",
777
- " continue",
778
- " if not re.match(r'^\\s', line) and line.endswith(':'):",
779
- " flush()",
780
- " header = line[:-1]",
781
- " names = []",
782
- " for part in header.split(','):",
783
- " p = part.strip()",
784
- ' if len(p)>=2 and p[0]==p[-1] and p[0] in "\'\\"": p = p[1:-1]',
785
- " at = p.find('@', 1) if p.startswith('@') else p.find('@')",
786
- " names.append(p[:at] if at > 0 else p)",
787
- " buf = [line]",
788
- " continue",
789
- " if names: buf.append(line)",
790
- " flush()",
791
- " return blocks",
792
- "def yarn_ok(base, head):",
793
- " bb, hb = yarn_blocks(base), yarn_blocks(head)",
794
- " for name in set(bb)|set(hb):",
795
- " if is_dir_key(name): continue",
796
- " if bb.get(name) != hb.get(name): return False",
797
- " return True",
798
- "paths = changed_names()",
799
- "core = [p for p in paths if p == '.deft/core' or p.startswith('.deft/core/')]",
800
- "if not core:",
801
- " sys.exit(0)",
802
- "checks = {",
803
- " 'package.json': pkg_pin_only,",
804
- " 'package-lock.json': npm_lock_ok,",
805
- " 'pnpm-lock.yaml': pnpm_ok,",
806
- " 'yarn.lock': yarn_ok,",
807
- "}",
808
- "bad = []",
809
- "for path, fn in checks.items():",
810
- " if path not in paths: continue",
811
- " if not fn(git_show(base_sha, path), git_show(head_sha, path)):",
812
- " bad.append(path)",
813
- "if bad:",
814
- " print('::error title=deft-core guard (#3193)::package/lock co-travel with .deft/core/** must be @deftai/directive* pin-only + lock follow-through. Rejected: ' + ', '.join(bad))",
815
- " print('--- content-rejected pin/lock paths ---')",
816
- " print('\\n'.join(bad))",
817
- " sys.exit(1)",
818
- "print('OK: package/lock content is Directive pin unit (#3193).')",
819
- ];
562
+ // Shared SoT with cmd/deft-install/core_guard_pin_content.embed (#3427).
563
+ // Consumer deposit stays a python3 heredoc; this repo has no .py file.
820
564
  const run = CORE_GUARD_RUN_INDENT;
565
+ const pyBody = CORE_GUARD_PIN_CONTENT_PYTHON.replace(/\n$/, "").split("\n");
821
566
  return [
822
567
  `${run}python3 - "$BASE_SHA" "$HEAD_SHA" <<'PY'`,
823
568
  ...pyBody.map((line) => `${run}${line}`),
@@ -848,8 +593,9 @@ function coreGuardWorkflowContent() {
848
593
  assertInstallerAllowlistHonors1430();
849
594
  const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
850
595
  const headSha = githubActionsExpr("github.event.pull_request.head.sha");
596
+ const baseRef = githubActionsExpr("github.event.pull_request.base.ref");
851
597
  return ("name: deft-core-guard\n\n" +
852
- "# Deft framework guard (#1430 / #3127 / #3193 / #3345): a single PR should not mix changes to the\n" +
598
+ "# Deft framework guard (#1430 / #3127 / #3193 / #3345 / #3388): a single PR should not mix changes to the\n" +
853
599
  "# vendored framework payload (.deft/core/**) with true application/product files.\n" +
854
600
  "# One upgrade PR MAY include deposit + installer-managed paths + package.json pin/lock\n" +
855
601
  "# + .deft/GENERATION.json when package.json is @deftai/directive* dependency-key pin-only\n" +
@@ -871,6 +617,7 @@ function coreGuardWorkflowContent() {
871
617
  " env:\n" +
872
618
  ` BASE_SHA: ${baseSha}\n` +
873
619
  ` HEAD_SHA: ${headSha}\n` +
620
+ ` BASE_REF: ${baseRef}\n` +
874
621
  " run: |\n" +
875
622
  " set -eu\n" +
876
623
  ' changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")\n' +
@@ -879,6 +626,8 @@ function coreGuardWorkflowContent() {
879
626
  " core=$(printf '%s\\n' \"$changed\" | grep -E '^\\.deft/core/' || true)\n" +
880
627
  `${coreGuardAllowlistShell()}\n` +
881
628
  ' if [ -n "$core" ] && [ -n "$app" ]; then\n' +
629
+ " # Evidence-based branch-sync exemption (#3388). Not name-only.\n" +
630
+ `${renderCoreGuardBranchSyncIfBlock(CORE_GUARD_RUN_INDENT)}\n` +
882
631
  ' echo "::error title=deft-core guard (#1430)::This PR changes the vendored framework payload (.deft/core/**) AND non-framework files. Split the framework update into its own PR."\n' +
883
632
  ' echo "--- framework (.deft/core/**) changes ---"; printf \'%s\\n\' "$core"\n' +
884
633
  ' echo "--- non-framework changes ---"; printf \'%s\\n\' "$app"\n' +
@@ -1053,7 +802,7 @@ export async function pruneFrameworkSelfTests(projectDir, io) {
1053
802
  catch {
1054
803
  return false;
1055
804
  }
1056
- await rm(path, { recursive: true, force: true });
805
+ containedRemove({ root: projectDir, target: path, recursive: true });
1057
806
  io.printf(`Removed vendored framework self-tests (${FRAMEWORK_SELF_TEST_REL}) from the consumer deposit (#1474).\n`);
1058
807
  return true;
1059
808
  }
@@ -1074,7 +823,7 @@ export async function pruneVendoredTsTests(projectDir, io) {
1074
823
  await walk(full);
1075
824
  }
1076
825
  else if (entry.isFile() && VENDORED_TS_TEST_RE.test(entry.name)) {
1077
- await rm(full, { force: true });
826
+ containedRemove({ root: projectDir, target: full });
1078
827
  removed += 1;
1079
828
  }
1080
829
  }