fix: address ds-review-bot round — insert-aliasing clones, settlement gates, closure module fallback

- Clone patch lists per generation (boot + composeLive): the include pushes
  insert rows by reference and mutates them in place, so a reused object
  baked user overrides into bundle rows and removal could not revert; the
  built-bin hot-reload e2e now asserts an override AND its removal reverting.
- The headless runner awaits Loader settlement before prompting (its inject
  gate covers only apiProxy/httpServer) and abandons cleanly when the tree
  died during the wait.
- healProfilesModuleFallback walks the app's full dependency+peer closure:
  out-of-tree plugins import seam packages (dsh-compact, dsh-subprocess, ...)
  that only implementations reach, and peers are how seams are declared.
- Profile init writes pnpm-workspace.yaml (nodeLinker: hoisted), not .npmrc
  — pnpm >=10 reads settings from the workspace manifest.
- Web dumps reject boot-only flags instead of printing a tree that differs
  from the same invocation's boot; --port validates at the flag;
  --dump-default-config no longer parses the (possibly broken) user layer;
  trustedHosts flag derivation merges over the composed value instead of
  replacing it; web-runtime gains surfaceContext (headless disables the GUI
  prompt/bash-vars the old -p never mounted); 'node_modules' is a reserved
  profile name; plugin-warning names the recovery step; client AGENTS.md
  registration surfaces point at the web-app bundle.
- Ship session-reference/tmux-context/tool-ask-user as app dependencies for
  terminal front-door patch layers (turtle-ui), same stance as mcp-client.
This commit is contained in:
Turtle
2026-08-06 09:27:44 +08:00
parent 273f27260d
commit 925daf141b
20 changed files with 240 additions and 83 deletions

View File

@@ -186,15 +186,22 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
composed.profile.layers.reduce((n, layer) => n + layer.patches.length, 0)
+ composed.profile.patches.length,
)
const composeLive = (profilePatches: PatchOptions[]): PatchOptions[] => [
// Fresh clones per generation: the include pushes `insert` rows into the
// mounted tree BY REFERENCE and later id-targeted patches mutate those
// objects in place. Reusing one parsed patch object across applications
// would bake a user override into the bundle's in-memory insert row, so
// removing the override could never revert the row to the bundle default.
const composeLive = (profilePatches: PatchOptions[]): PatchOptions[] => structuredClone([
...composed.profile.layers.flatMap(layer => layer.patches),
...profilePatches,
...overlayAndFlags,
]
])
// One-shot runs exit through the runner; watching would only hold the
// process open after its exit request.
const watchProfilePatch = options.task === undefined
const ctx = await boot(NAME, rootConfig, composed.patches, async (hostCtx) => {
// Cloned for the same insert-aliasing reason as composeLive: the boot
// application must not mutate the objects later reloads recompose from.
const ctx = await boot(NAME, rootConfig, structuredClone(composed.patches), async (hostCtx) => {
app.current = hostCtx
if (options.task !== undefined) {
const io: HeadlessIo = {