create-walle 0.10.2 → 0.10.3
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.
- package/README.md +3 -1
- package/lib/release-schema.json +2 -2
- package/lib/runtime-closure-policy.json +8 -7
- package/package.json +1 -1
- package/template/coding-task-manager/api-prompts.js +45 -7
- package/template/coding-task-manager/bin/claude-hook.js +55 -1
- package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
- package/template/coding-task-manager/bin/land.js +2069 -0
- package/template/coding-task-manager/bin/memory-census.js +4 -2
- package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
- package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
- package/template/coding-task-manager/db.js +156 -32
- package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
- package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
- package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
- package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
- package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
- package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
- package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
- package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
- package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
- package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
- package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
- package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
- package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
- package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
- package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
- package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
- package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
- package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
- package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
- package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
- package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
- package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
- package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
- package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
- package/template/coding-task-manager/lib/agent-definitions.js +19 -0
- package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
- package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
- package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
- package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
- package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
- package/template/coding-task-manager/lib/agent-presets.js +84 -1
- package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
- package/template/coding-task-manager/lib/agent-version-service.js +74 -11
- package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
- package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
- package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
- package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
- package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
- package/template/coding-task-manager/lib/api/fleet.js +118 -0
- package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
- package/template/coding-task-manager/lib/api/ingress.js +9 -1
- package/template/coding-task-manager/lib/api/landing.js +403 -0
- package/template/coding-task-manager/lib/api/machines.js +691 -2
- package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
- package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
- package/template/coding-task-manager/lib/api/session-attach.js +623 -0
- package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
- package/template/coding-task-manager/lib/api/sessions.js +77 -51
- package/template/coding-task-manager/lib/api/setup-network.js +544 -16
- package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
- package/template/coding-task-manager/lib/api/setup.js +829 -10
- package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
- package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
- package/template/coding-task-manager/lib/auth-rules.js +292 -21
- package/template/coding-task-manager/lib/background-stop.js +369 -0
- package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
- package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
- package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
- package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
- package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
- package/template/coding-task-manager/lib/build-info.js +4 -2
- package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
- package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
- package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
- package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
- package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
- package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
- package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
- package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
- package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
- package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
- package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
- package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
- package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
- package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
- package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
- package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
- package/template/coding-task-manager/lib/conversation-sync.js +355 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
- package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
- package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
- package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
- package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
- package/template/coding-task-manager/lib/db-domains.js +1 -0
- package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
- package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
- package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
- package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
- package/template/coding-task-manager/lib/feature-manifest.js +4 -0
- package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
- package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
- package/template/coding-task-manager/lib/fleet-hub.js +762 -0
- package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
- package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
- package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
- package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
- package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
- package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
- package/template/coding-task-manager/lib/fleet-update.js +798 -0
- package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
- package/template/coding-task-manager/lib/fs-watcher.js +58 -42
- package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
- package/template/coding-task-manager/lib/headless-term-service.js +84 -8
- package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
- package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
- package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
- package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
- package/template/coding-task-manager/lib/host-handoff.js +16 -19
- package/template/coding-task-manager/lib/host-stats.js +150 -3
- package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
- package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
- package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
- package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
- package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
- package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
- package/template/coding-task-manager/lib/hub-bridge.js +398 -0
- package/template/coding-task-manager/lib/idle-notify.js +491 -0
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
- package/template/coding-task-manager/lib/instance-manifest.js +5 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
- package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
- package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
- package/template/coding-task-manager/lib/landing/config.js +536 -0
- package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
- package/template/coding-task-manager/lib/landing/gate.js +360 -0
- package/template/coding-task-manager/lib/landing/git.js +344 -0
- package/template/coding-task-manager/lib/landing/lease.js +247 -0
- package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
- package/template/coding-task-manager/lib/landing/proof.js +223 -0
- package/template/coding-task-manager/lib/landing/queue.js +998 -0
- package/template/coding-task-manager/lib/landing/radar.js +489 -0
- package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
- package/template/coding-task-manager/lib/landing/state.js +263 -0
- package/template/coding-task-manager/lib/landing/stop.js +49 -0
- package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
- package/template/coding-task-manager/lib/landing/sync.js +456 -0
- package/template/coding-task-manager/lib/landing/train.js +885 -0
- package/template/coding-task-manager/lib/landing/window.js +57 -0
- package/template/coding-task-manager/lib/launch-environment.js +121 -0
- package/template/coding-task-manager/lib/launch-model-override.js +36 -10
- package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
- package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
- package/template/coding-task-manager/lib/lead-model-options.js +160 -3
- package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
- package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
- package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
- package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
- package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
- package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
- package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
- package/template/coding-task-manager/lib/machine-registry.js +750 -5
- package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
- package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
- package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
- package/template/coding-task-manager/lib/memory-join.js +337 -0
- package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
- package/template/coding-task-manager/lib/memory-sync.js +565 -0
- package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
- package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
- package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
- package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
- package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
- package/template/coding-task-manager/lib/model-availability.js +727 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
- package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
- package/template/coding-task-manager/lib/model-id-rule.js +19 -0
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
- package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
- package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
- package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
- package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
- package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
- package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
- package/template/coding-task-manager/lib/native-history-control.js +6 -4
- package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
- package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
- package/template/coding-task-manager/lib/opencode-cli.js +480 -0
- package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
- package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
- package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
- package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
- package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
- package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
- package/template/coding-task-manager/lib/perf-summary.js +7 -0
- package/template/coding-task-manager/lib/perf-tracker.js +63 -2
- package/template/coding-task-manager/lib/project-config.js +7 -0
- package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
- package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
- package/template/coding-task-manager/lib/read-pool-client.js +89 -2
- package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
- package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
- package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
- package/template/coding-task-manager/lib/relay-client.js +57 -3
- package/template/coding-task-manager/lib/relink-audit.js +34 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
- package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
- package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
- package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
- package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
- package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
- package/template/coding-task-manager/lib/restart-guard.js +18 -6
- package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
- package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
- package/template/coding-task-manager/lib/resume-admission.js +17 -3
- package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
- package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
- package/template/coding-task-manager/lib/runtime-contract.js +3 -1
- package/template/coding-task-manager/lib/schema-indexes.js +1 -0
- package/template/coding-task-manager/lib/session-context-locator.js +742 -0
- package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
- package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
- package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
- package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
- package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
- package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
- package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
- package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
- package/template/coding-task-manager/lib/session-create/response.js +7 -7
- package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
- package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
- package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
- package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
- package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
- package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
- package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
- package/template/coding-task-manager/lib/session-empty.js +13 -10
- package/template/coding-task-manager/lib/session-host-manager.js +60 -0
- package/template/coding-task-manager/lib/session-jobs.js +3 -1
- package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
- package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
- package/template/coding-task-manager/lib/session-park.js +738 -0
- package/template/coding-task-manager/lib/session-path-grant.js +13 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
- package/template/coding-task-manager/lib/session-transport.js +17 -0
- package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
- package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
- package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
- package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
- package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
- package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
- package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
- package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
- package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
- package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
- package/template/coding-task-manager/lib/submission-ack.js +90 -2
- package/template/coding-task-manager/lib/systemd-scope.js +115 -0
- package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
- package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
- package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
- package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
- package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
- package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
- package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
- package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
- package/template/coding-task-manager/lib/transcript-source.js +36 -4
- package/template/coding-task-manager/lib/transcript-store.js +9 -0
- package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
- package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
- package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
- package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
- package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
- package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
- package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
- package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
- package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
- package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
- package/template/coding-task-manager/lib/walle/turn.js +1 -1
- package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
- package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
- package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
- package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
- package/template/coding-task-manager/lib/walle-row-history.js +495 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
- package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
- package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
- package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
- package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
- package/template/coding-task-manager/lib/ws/misc.js +1 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
- package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
- package/template/coding-task-manager/oauth-proxy.js +112 -115
- package/template/coding-task-manager/package-lock.json +10 -94
- package/template/coding-task-manager/package.json +4 -3
- package/template/coding-task-manager/protocol/message-table.json +10 -0
- package/template/coding-task-manager/protocol/messages.js +10 -0
- package/template/coding-task-manager/protocol/ws-schema.json +20 -0
- package/template/coding-task-manager/protocol/ws-schema.md +2 -0
- package/template/coding-task-manager/public/css/app.css +170 -0
- package/template/coding-task-manager/public/css/walle-session.css +4 -0
- package/template/coding-task-manager/public/index.html +473 -243
- package/template/coding-task-manager/public/js/agent-command.js +86 -0
- package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
- package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
- package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
- package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
- package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
- package/template/coding-task-manager/public/js/background-work.js +196 -27
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
- package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
- package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
- package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
- package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
- package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
- package/template/coding-task-manager/public/js/document-review-links.js +9 -1
- package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
- package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
- package/template/coding-task-manager/public/js/message-renderer.js +4 -3
- package/template/coding-task-manager/public/js/model-picker.js +3 -1
- package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
- package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
- package/template/coding-task-manager/public/js/pasted-content.js +236 -0
- package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
- package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
- package/template/coding-task-manager/public/js/prompts.js +11 -10
- package/template/coding-task-manager/public/js/protocol.js +10 -0
- package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
- package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
- package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
- package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
- package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
- package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
- package/template/coding-task-manager/public/js/stream-view.js +6 -5
- package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
- package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
- package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
- package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
- package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
- package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
- package/template/coding-task-manager/public/js/walle.js +6 -0
- package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
- package/template/coding-task-manager/public/m/app.css +140 -0
- package/template/coding-task-manager/public/m/app.js +507 -68
- package/template/coding-task-manager/public/m/index.html +17 -0
- package/template/coding-task-manager/public/m/machines.js +838 -4
- package/template/coding-task-manager/public/m/private-api.js +31 -1
- package/template/coding-task-manager/public/usage.html +1 -0
- package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
- package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
- package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
- package/template/coding-task-manager/server.js +1950 -11679
- package/template/coding-task-manager/session-integrity.js +64 -4
- package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
- package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
- package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
- package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
- package/template/coding-task-manager/workers/session-host-process.js +5 -5
- package/template/coding-task-manager/workers/watch-worker.js +105 -0
- package/template/coding-task-manager/workers/worktree-worker.js +168 -33
- package/template/ctm-relay/lib/protocol.js +14 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/shared/backup-retention.js +82 -0
- package/template/shared/conversation-admission.js +2 -0
- package/template/shared/conversation-provenance.js +3 -7
- package/template/shared/conversation-storage-v2-contract.js +5 -0
- package/template/shared/linked-worktree.js +51 -0
- package/template/shared/macos-fda-target.js +57 -0
- package/template/shared/orphan-parents.js +55 -0
- package/template/shared/release-schema.json +2 -2
- package/template/shared/storage/contract.js +3 -0
- package/template/wall-e/adapters/imessage.js +12 -0
- package/template/wall-e/agent.js +71 -4
- package/template/wall-e/api-walle.js +21 -1
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
- package/template/wall-e/brain/brain-join.js +859 -0
- package/template/wall-e/brain/brain-sync-membership.js +20 -0
- package/template/wall-e/brain/brain-sync-store.js +131 -77
- package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
- package/template/wall-e/brain/brain-sync-v2.js +578 -0
- package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
- package/template/wall-e/brain/deterministic-id.js +51 -0
- package/template/wall-e/brain/memories.js +2 -2
- package/template/wall-e/brain/migrations.js +40 -6
- package/template/wall-e/brain/model-identity-migration.js +1 -1
- package/template/wall-e/brain/owner-call-whitelist.js +23 -7
- package/template/wall-e/brain/schema.js +45 -85
- package/template/wall-e/brain.js +171 -35
- package/template/wall-e/chat.js +3 -3
- package/template/wall-e/coding/agent-shell-env.js +7 -2
- package/template/wall-e/coding/prompt-bundle.js +66 -21
- package/template/wall-e/config/main-loop-exemptions.json +2 -2
- package/template/wall-e/http/admin-api.js +37 -0
- package/template/wall-e/http/gws-api.js +4 -0
- package/template/wall-e/http/helper-rules-api.js +7 -1
- package/template/wall-e/http/model-admin.js +8 -1
- package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
- package/template/wall-e/http/model-proxy-providers.js +174 -1
- package/template/wall-e/http/model-route-health.js +157 -0
- package/template/wall-e/http/sync-api.js +278 -464
- package/template/wall-e/jobs/job-runner.js +28 -1
- package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
- package/template/wall-e/lib/ctm-data-dir.js +26 -0
- package/template/wall-e/lib/derivation-home.js +147 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
- package/template/wall-e/lib/helper-rules/grade.js +51 -8
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
- package/template/wall-e/lib/helper-rules/store.js +4 -2
- package/template/wall-e/lib/landing-mcp-tools.js +170 -0
- package/template/wall-e/lib/mcp-client-config.js +133 -14
- package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
- package/template/wall-e/lib/mcp-integration.js +58 -13
- package/template/wall-e/lib/parent-death.js +7 -1
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
- package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
- package/template/wall-e/llm/delegate-vendor-default.js +12 -5
- package/template/wall-e/llm/model-roles.js +1 -1
- package/template/wall-e/llm/native-delegate-selection.js +56 -8
- package/template/wall-e/llm/worker-delegate-args.js +72 -7
- package/template/wall-e/llm/worker-mcp-routing.js +66 -17
- package/template/wall-e/loops/brain-custom-writes.js +19 -0
- package/template/wall-e/loops/ingest.js +17 -1
- package/template/wall-e/loops/think.js +19 -4
- package/template/wall-e/mcp-contract.js +222 -6
- package/template/wall-e/mcp-protocol-contract.js +40 -5
- package/template/wall-e/mcp-server.js +128 -29
- package/template/wall-e/mcp-worker-schemas.js +5 -5
- package/template/wall-e/memory/ctm-context-client.js +85 -0
- package/template/wall-e/memory/ctm-session-context.js +9 -3
- package/template/wall-e/memory/hub-bridge-client.js +388 -0
- package/template/wall-e/memory/replica-origin.js +56 -0
- package/template/wall-e/memory/session-pool.js +135 -0
- package/template/wall-e/runtime/live-capabilities.js +8 -3
- package/template/wall-e/server.js +4 -2
- package/template/wall-e/shared/release-schema.json +2 -2
- package/template/wall-e/shared/sqlite-file-set.js +39 -0
- package/template/wall-e/shared/storage/contract.js +3 -0
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
- package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
- package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
- package/template/wall-e/tools/chrome-automation.js +6 -0
- package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
- package/template/wall-e/workers/brain-owner-worker.js +18 -4
- package/template/wall-e/workers/mcp-gateway-process.js +5 -5
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// WP-1 (docs/local-landing-queue-multi-session-integration-2026-10-26.html §5 D-3/D-5/D-8, §11
|
|
3
|
+
// Risks, §8 WP-1): the train — how a job becomes exactly one commit to fast-forward the target to.
|
|
4
|
+
//
|
|
5
|
+
// D-3 builds the train WITHOUT touching any worktree (`merge-tree --write-tree` + `commit-tree`);
|
|
6
|
+
// the only worktree this module ever creates is the job's own gate worktree, and only D-8 (a
|
|
7
|
+
// conflict confined to `landing.regenerate` paths) needs it for the commit itself. Nothing here
|
|
8
|
+
// writes into a session worktree, and nothing here ever merges *into* the target checkout unguarded
|
|
9
|
+
// — D-5 goes through `git merge --ff-only`, which git itself refuses when it would clobber local
|
|
10
|
+
// state, or through the compare-and-swap `git update-ref <ref> <new> <old>`.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { spawn } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
landingError,
|
|
19
|
+
childEnv,
|
|
20
|
+
commonDir,
|
|
21
|
+
runGitRaw,
|
|
22
|
+
git,
|
|
23
|
+
gitFailed,
|
|
24
|
+
revParse,
|
|
25
|
+
revParseOrNull,
|
|
26
|
+
isAncestor,
|
|
27
|
+
mergeTree,
|
|
28
|
+
commitTree,
|
|
29
|
+
casUpdateRef,
|
|
30
|
+
ffCheckedOut,
|
|
31
|
+
} = require('./git');
|
|
32
|
+
|
|
33
|
+
const MAX_ROUNDS = 3;
|
|
34
|
+
const GLOB_CACHE = new Map();
|
|
35
|
+
// A gate that ignores SIGTERM gets this long before its whole process group is SIGKILLed.
|
|
36
|
+
const GATE_KILL_GRACE_MS = 10000;
|
|
37
|
+
|
|
38
|
+
function targetRef(target) {
|
|
39
|
+
return `refs/heads/${target}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Signal a whole process tree. `detached: true` made the shell a process-group leader, so the
|
|
43
|
+
// negative pid reaches every descendant the gate spawned — a bare `child.kill()` reaches only
|
|
44
|
+
// `/bin/sh` and leaves the test suite the gate started running forever (D-4's `gate_timeout` then
|
|
45
|
+
// reports a timeout while the machine keeps working).
|
|
46
|
+
function killGroup(child, signal) {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(-child.pid, signal);
|
|
49
|
+
return true;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
/* expected: the group is already gone, or the pid never became a group leader — fall back to
|
|
52
|
+
the direct pid, which is also how a non-detached child would be signalled */
|
|
53
|
+
try {
|
|
54
|
+
child.kill(signal);
|
|
55
|
+
return true;
|
|
56
|
+
} catch (inner) {
|
|
57
|
+
/* expected: the process exited between the signal and this call — nothing left to signal */
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Every gate/regenerate command the queue runs goes through `/bin/sh -c` with an env built from
|
|
64
|
+
// `process.env` MINUS the seven §4 GIT_* names (`childEnv`), plus the LAND_* contract. `extra`
|
|
65
|
+
// cannot reintroduce a scrubbed name: `childEnv` applies it AFTER the scrub but the keys below are
|
|
66
|
+
// never GIT_*.
|
|
67
|
+
//
|
|
68
|
+
// D-4 keeps the gate a CHILD, never the runner itself, and it is asynchronous on purpose:
|
|
69
|
+
// `spawn` (not `spawnSync`) leaves the runner's event loop free, so the lease heartbeat really
|
|
70
|
+
// fires while a 40-minute gate works. Output streams straight into the job log file through
|
|
71
|
+
// `logFd` — a live `land inspect` tail — instead of accumulating in a 64 MiB in-memory buffer.
|
|
72
|
+
// WP-2 (§5 D-5): `signal` is an optional AbortSignal. The queue's stop primitive aborts it from the
|
|
73
|
+
// runner's heartbeat, and every command of the job — gates AND regenerate entries — runs through
|
|
74
|
+
// here, so this one option is the single place a running gate can be stopped. An already-aborted
|
|
75
|
+
// signal resolves WITHOUT spawning, and an abort mid-run signals the whole process group exactly the
|
|
76
|
+
// way the timeout path does (SIGTERM, then SIGKILL after `GATE_KILL_GRACE_MS`).
|
|
77
|
+
function runShell(command, { cwd, extra, timeoutMs, logFd, signal } = {}) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const started = Date.now();
|
|
80
|
+
if (signal && signal.aborted) {
|
|
81
|
+
resolve({ status: null, signal: null, timedOut: false, aborted: true, error: null, seconds: 0 });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const stdio = logFd === undefined || logFd === null ? 'ignore' : logFd;
|
|
85
|
+
let child;
|
|
86
|
+
try {
|
|
87
|
+
child = spawn('/bin/sh', ['-c', command], {
|
|
88
|
+
cwd,
|
|
89
|
+
env: childEnv(extra),
|
|
90
|
+
detached: true,
|
|
91
|
+
stdio: ['ignore', stdio, stdio],
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
resolve({ status: null, signal: null, timedOut: false, aborted: false, error, seconds: 0 });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let timedOut = false;
|
|
98
|
+
let aborted = false;
|
|
99
|
+
let killTimer = null;
|
|
100
|
+
const timer = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
101
|
+
? setTimeout(() => {
|
|
102
|
+
timedOut = true;
|
|
103
|
+
killGroup(child, 'SIGTERM');
|
|
104
|
+
killTimer = setTimeout(() => killGroup(child, 'SIGKILL'), GATE_KILL_GRACE_MS);
|
|
105
|
+
}, timeoutMs)
|
|
106
|
+
: null;
|
|
107
|
+
const onAbort = () => {
|
|
108
|
+
aborted = true;
|
|
109
|
+
killGroup(child, 'SIGTERM');
|
|
110
|
+
killTimer = setTimeout(() => killGroup(child, 'SIGKILL'), GATE_KILL_GRACE_MS);
|
|
111
|
+
};
|
|
112
|
+
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
113
|
+
const finish = (status, termSignal, error) => {
|
|
114
|
+
if (timer) clearTimeout(timer);
|
|
115
|
+
if (killTimer) clearTimeout(killTimer);
|
|
116
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
117
|
+
resolve({
|
|
118
|
+
status,
|
|
119
|
+
signal: termSignal,
|
|
120
|
+
timedOut,
|
|
121
|
+
aborted,
|
|
122
|
+
error: error || null,
|
|
123
|
+
seconds: Number(((Date.now() - started) / 1000).toFixed(3)),
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
child.on('error', (error) => finish(null, null, error));
|
|
127
|
+
child.on('close', (status, termSignal) => finish(status, termSignal, null));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- globs (§6.1 `regenerate[].paths`, `registries[]`) ----------------------------------------
|
|
132
|
+
|
|
133
|
+
// Git-style pathspec globbing: `**` crosses directory boundaries (including zero of them, so
|
|
134
|
+
// `docs/**` matches `docs/INDEX.html`), `*` and `?` do not.
|
|
135
|
+
function globToRegExp(pattern) {
|
|
136
|
+
const cached = GLOB_CACHE.get(pattern);
|
|
137
|
+
if (cached) return cached;
|
|
138
|
+
let out = '^';
|
|
139
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
140
|
+
const ch = pattern[index];
|
|
141
|
+
if (ch === '*') {
|
|
142
|
+
if (pattern[index + 1] === '*') {
|
|
143
|
+
if (pattern[index + 2] === '/') {
|
|
144
|
+
out += '(?:[^/]*/)*';
|
|
145
|
+
index += 2;
|
|
146
|
+
} else {
|
|
147
|
+
out += '.*';
|
|
148
|
+
index += 1;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
out += '[^/]*';
|
|
152
|
+
}
|
|
153
|
+
} else if (ch === '?') {
|
|
154
|
+
out += '[^/]';
|
|
155
|
+
} else if ('\\^$.|+()[]{}'.includes(ch)) {
|
|
156
|
+
out += `\\${ch}`;
|
|
157
|
+
} else {
|
|
158
|
+
out += ch;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const re = new RegExp(`${out}$`);
|
|
162
|
+
GLOB_CACHE.set(pattern, re);
|
|
163
|
+
return re;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function globMatch(pattern, file) {
|
|
167
|
+
if (globToRegExp(pattern).test(file)) return true;
|
|
168
|
+
// A directory pattern (`docs`, `docs/`) also owns everything underneath it.
|
|
169
|
+
return globToRegExp(`${pattern.replace(/\/+$/, '')}/**`).test(file);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// D-2 (§9 S-6): every `landing.refresh[]` entry runs on EVERY train. The entry's owned paths are
|
|
173
|
+
// tree-derived pins (the closure policy's `sourceBindings` digests), and the bound set is the
|
|
174
|
+
// policy's own key list — "did a bound source change" is not knowable cheaply from a path list, and
|
|
175
|
+
// the script is idempotent, so the simplest correct rule is to run all of them and let the
|
|
176
|
+
// `git diff --cached --quiet` check decide whether a new commit is needed at all.
|
|
177
|
+
function matchingRefreshEntries(config) {
|
|
178
|
+
return config && Array.isArray(config.refresh) ? config.refresh : [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// D-8: every conflicted path must be owned by a `landing.regenerate[]` entry. Returns the distinct
|
|
182
|
+
// entries to run, or null when at least one path is hand-written (the job blocks).
|
|
183
|
+
function matchingRegenerateEntries(config, conflicts) {
|
|
184
|
+
const entries = config && Array.isArray(config.regenerate) ? config.regenerate : [];
|
|
185
|
+
const matched = [];
|
|
186
|
+
for (const file of conflicts) {
|
|
187
|
+
const entry = entries.find((candidate) => candidate.paths.some((pattern) => globMatch(pattern, file)));
|
|
188
|
+
if (!entry) return null;
|
|
189
|
+
if (!matched.includes(entry)) matched.push(entry);
|
|
190
|
+
}
|
|
191
|
+
return matched;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// MOVED here from `gate.js` verbatim (WP-2, §5 D-6) so the base rerun can build the same worktree:
|
|
195
|
+
// the cache directories a gate needs (`landing.cachePaths`, e.g. `node_modules`) are symlinked into
|
|
196
|
+
// the materialized worktree rather than copied. The source checkout only supplies them when it has
|
|
197
|
+
// them — a fresh clone with no `node_modules` simply has nothing to link in — and a path that is a
|
|
198
|
+
// REAL tracked directory in the tree wins over the link.
|
|
199
|
+
function linkCachePaths(dir, sourceDir, cachePaths) {
|
|
200
|
+
const linked = [];
|
|
201
|
+
for (const rel of cachePaths) {
|
|
202
|
+
const source = path.resolve(sourceDir, rel);
|
|
203
|
+
let stat = null;
|
|
204
|
+
try {
|
|
205
|
+
stat = fs.statSync(source);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
/* expected: this cache directory has not been populated in the source checkout (a fresh
|
|
208
|
+
clone); there is simply nothing to link in */
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!stat.isDirectory()) continue;
|
|
212
|
+
const link = path.join(dir, rel);
|
|
213
|
+
fs.mkdirSync(path.dirname(link), { recursive: true });
|
|
214
|
+
try {
|
|
215
|
+
fs.symlinkSync(source, link, 'dir');
|
|
216
|
+
linked.push(rel);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
/* expected: the train tree already contains a real directory at this path (the cache path is
|
|
219
|
+
tracked in this repository after all) — the real directory wins */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return linked;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- D-3: the worktree-free train -------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
// `{ kind, base, tree, train, conflicts }` where kind is:
|
|
228
|
+
// 'unchanged' — the merge result is the target's own tree: the branch is already in the target
|
|
229
|
+
// 'train' — `train` is the commit to fast-forward the target to (the tip itself when the
|
|
230
|
+
// target is an ancestor of it, otherwise a two-parent merge commit)
|
|
231
|
+
// 'conflict' — `conflicts` holds the paths merge-tree reported
|
|
232
|
+
function buildTrain(cwd, target, job) {
|
|
233
|
+
const built = buildBatchTrain(cwd, target, [job]);
|
|
234
|
+
const skipped = built.skipped.length ? built.skipped[0] : null;
|
|
235
|
+
if (skipped) {
|
|
236
|
+
return { kind: 'conflict', base: built.base, tree: built.tree, train: null, conflicts: skipped.paths };
|
|
237
|
+
}
|
|
238
|
+
if (built.kind === 'unchanged') {
|
|
239
|
+
return { kind: 'unchanged', base: built.base, tree: built.tree, train: built.base, conflicts: [] };
|
|
240
|
+
}
|
|
241
|
+
return { kind: 'train', base: built.base, tree: built.tree, train: built.train, conflicts: [] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// WP-1 D-1 (§5 D-1, §6, §8 WP-1, §9 S-2, AC-1a/AC-1c): the batched train. `c0 = base` (the target
|
|
245
|
+
// tip) and each member in queue order is merged ON TOP of the previous chain head:
|
|
246
|
+
// c_k = commitTree(mergeTree(c_{k-1}, tip_k), [c_{k-1}, tip_k])
|
|
247
|
+
// with the fast-forward shortcut `buildTrain` already had: when `tip_k` descends from `c_{k-1}` the
|
|
248
|
+
// train is `tip_k` itself, so a member that is simply ahead of the target keeps the commit the
|
|
249
|
+
// author's own gate proved (D-6's proof chain walks first parents and stays valid).
|
|
250
|
+
//
|
|
251
|
+
// A member whose mergeTree onto `c_{k-1}` CONFLICTS is skipped — `onSkip(member, paths)` is called,
|
|
252
|
+
// the chain does not advance, and the next member is merged onto the same head (D-1: the conflicting
|
|
253
|
+
// member never blocks the others; it goes back to the queue with `{ base: c_{k-1}, paths }` as its
|
|
254
|
+
// preview). A chain that never moves (every member skipped, or every merge a no-op) is `unchanged`.
|
|
255
|
+
//
|
|
256
|
+
// Returns `{ kind, base, members, skipped, chain, train, tree }`: `chain[k]` is the commit for
|
|
257
|
+
// `members[0..k]` minus the skipped ones, `train` the final head (== `base` when nothing moved).
|
|
258
|
+
function buildBatchTrain(cwd, target, members, opts = {}) {
|
|
259
|
+
const onSkip = typeof opts.onSkip === 'function' ? opts.onSkip : () => {};
|
|
260
|
+
const base = revParse(cwd, targetRef(target));
|
|
261
|
+
const baseTree = revParse(cwd, `${base}^{tree}`);
|
|
262
|
+
const list = Array.isArray(members) ? members : [];
|
|
263
|
+
const chain = [];
|
|
264
|
+
const skipped = [];
|
|
265
|
+
const kept = [];
|
|
266
|
+
let head = base;
|
|
267
|
+
for (const member of list) {
|
|
268
|
+
const merged = mergeTree(cwd, head, member.tip);
|
|
269
|
+
if (merged.conflicts.length) {
|
|
270
|
+
// `head` is the chain commit the conflict was measured against, which is what the member's
|
|
271
|
+
// `conflictPreview.base` must name (D-1); the design's signature is `onSkip(member, paths)` and
|
|
272
|
+
// the extra argument is additive.
|
|
273
|
+
onSkip(member, merged.conflicts, head);
|
|
274
|
+
skipped.push({ id: member.id, paths: merged.conflicts });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (isAncestor(cwd, head, member.tip)) {
|
|
278
|
+
// D-1 ff case, exactly as `buildTrain` does for one job: the train IS the member's own commit.
|
|
279
|
+
head = member.tip;
|
|
280
|
+
chain.push(head);
|
|
281
|
+
kept.push(member);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
head = commitTree(cwd, merged.tree, [head, member.tip], trainMessage(member));
|
|
285
|
+
chain.push(head);
|
|
286
|
+
kept.push(member);
|
|
287
|
+
}
|
|
288
|
+
const tree = revParse(cwd, `${head}^{tree}`);
|
|
289
|
+
const moved = chain.length > 0 && tree !== baseTree;
|
|
290
|
+
return {
|
|
291
|
+
kind: moved ? 'train' : 'unchanged',
|
|
292
|
+
base,
|
|
293
|
+
members: kept,
|
|
294
|
+
skipped,
|
|
295
|
+
chain,
|
|
296
|
+
train: moved ? head : base,
|
|
297
|
+
tree,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function trainMessage(job) {
|
|
302
|
+
return `land(${job.branch}): ${job.title}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// --- the gate worktree (§11 Risks) -------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
// §11: a gate that depends on its worktree path must see the repository root it expects, so the
|
|
308
|
+
// train worktree lives at `<repo-root>/.worktrees/land-<jobId>` whenever the common dir is exactly
|
|
309
|
+
// `<repo-root>/.git` AND `.worktrees/` is already git-ignored there (so it never shows up as
|
|
310
|
+
// untracked dirt in the repository). Otherwise it falls back to `<common-dir>/landing/trains/`.
|
|
311
|
+
function trainsRoot(cwd, commonDirPath) {
|
|
312
|
+
if (path.basename(commonDirPath) === '.git') {
|
|
313
|
+
const repoRoot = path.dirname(commonDirPath);
|
|
314
|
+
const probe = runGitRaw(repoRoot, ['check-ignore', '-q', '--', '.worktrees/land-probe']);
|
|
315
|
+
if (probe.status === 0) return { dir: path.join(repoRoot, '.worktrees'), source: 'repo-root' };
|
|
316
|
+
}
|
|
317
|
+
return { dir: path.join(commonDirPath, 'landing', 'trains'), source: 'common-dir' };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// WP-1 D-1 (§9 S-3): keyed by the TRAIN id — the batch id (`b-<ulid>`) for a batch, the job id for
|
|
321
|
+
// a singleton. One train, one worktree, one log, one selection record: every member of a batch points
|
|
322
|
+
// at the same artefacts, which is what makes the gate's output readable as a batch rather than as N
|
|
323
|
+
// copies of the first job's.
|
|
324
|
+
function trainDirFor(rootDir, trainId) {
|
|
325
|
+
return path.join(rootDir, `land-${trainId}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// A previous runner may have been killed between `worktree add` and its cleanup, leaving both the
|
|
329
|
+
// directory and its registration behind. Only paths that look like ours are touched.
|
|
330
|
+
function prepareTrainDir(cwd, dir) {
|
|
331
|
+
if (path.basename(dir).startsWith('land-') === false) {
|
|
332
|
+
throw landingError('train_dir_invalid', `refusing to prepare ${dir}: not a land-* train directory`, { dir });
|
|
333
|
+
}
|
|
334
|
+
runGitRaw(cwd, ['worktree', 'prune']);
|
|
335
|
+
let exists = true;
|
|
336
|
+
try {
|
|
337
|
+
fs.lstatSync(dir);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
/* expected: the common case — nobody left a train worktree behind for this job */
|
|
340
|
+
exists = false;
|
|
341
|
+
}
|
|
342
|
+
if (!exists) return false;
|
|
343
|
+
const removed = runGitRaw(cwd, ['worktree', 'remove', '--force', '--force', dir]);
|
|
344
|
+
if (removed.status !== 0) {
|
|
345
|
+
// A directory that is no longer registered as a worktree (the registration was pruned) is
|
|
346
|
+
// still ours by name and location; remove the leftover so `worktree add` can succeed.
|
|
347
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
348
|
+
}
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function addTrainWorktree(cwd, dir, train, jobId) {
|
|
353
|
+
prepareTrainDir(cwd, dir);
|
|
354
|
+
const args = ['worktree', 'add', '--detach', '--lock', '--reason', `land job ${jobId}`, dir, train];
|
|
355
|
+
const res = runGitRaw(cwd, args);
|
|
356
|
+
if (res.status !== 0) throw gitFailed(cwd, args, res);
|
|
357
|
+
return dir;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Only ever called for this job's own train directory (`<trains>/.worktrees|trains/land-<id>`),
|
|
361
|
+
// which the queue created itself and which no session can have work in. `--force --force` is
|
|
362
|
+
// required because the gate legitimately owns that directory and may have dirtied the tree inside
|
|
363
|
+
// it — AC-1d asserts the worktree is gone after `gate_dirtied_tree`.
|
|
364
|
+
function removeTrainWorktree(cwd, dir) {
|
|
365
|
+
if (!dir) return { removed: false, reason: 'no_dir' };
|
|
366
|
+
const res = runGitRaw(cwd, ['worktree', 'remove', '--force', '--force', dir]);
|
|
367
|
+
if (res.status === 0) return { removed: true };
|
|
368
|
+
let exists = true;
|
|
369
|
+
try {
|
|
370
|
+
fs.lstatSync(dir);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
/* expected: git already removed the directory but reported a non-zero status (for example a
|
|
373
|
+
concurrent `worktree prune`) — the directory is gone, which is what the caller asked for */
|
|
374
|
+
exists = false;
|
|
375
|
+
}
|
|
376
|
+
if (!exists) return { removed: true, note: 'already absent' };
|
|
377
|
+
return { removed: false, reason: String(res.stderr || res.stdout || '').trim() || `git worktree remove exited ${res.status}` };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// `git worktree list --porcelain`, parsed into one entry per worktree.
|
|
381
|
+
function worktreeList(cwd) {
|
|
382
|
+
const args = ['worktree', 'list', '--porcelain'];
|
|
383
|
+
const res = runGitRaw(cwd, args);
|
|
384
|
+
if (res.status !== 0) throw gitFailed(cwd, args, res);
|
|
385
|
+
const entries = [];
|
|
386
|
+
let current = null;
|
|
387
|
+
for (const line of res.stdout.split('\n')) {
|
|
388
|
+
if (line === '') {
|
|
389
|
+
if (current) entries.push(current);
|
|
390
|
+
current = null;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const space = line.indexOf(' ');
|
|
394
|
+
const key = space === -1 ? line : line.slice(0, space);
|
|
395
|
+
const value = space === -1 ? '' : line.slice(space + 1);
|
|
396
|
+
if (key === 'worktree') {
|
|
397
|
+
if (current) entries.push(current);
|
|
398
|
+
current = { path: value, branch: null, detached: false, locked: false, bare: false, prunable: false };
|
|
399
|
+
} else if (current) {
|
|
400
|
+
if (key === 'branch') current.branch = value;
|
|
401
|
+
else if (key === 'detached') current.detached = true;
|
|
402
|
+
else if (key === 'locked') current.locked = true;
|
|
403
|
+
else if (key === 'bare') current.bare = true;
|
|
404
|
+
else if (key === 'prunable') current.prunable = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (current) entries.push(current);
|
|
408
|
+
return entries;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// D-5: the worktree (if any) that has the target branch checked out. Detached worktrees — the
|
|
412
|
+
// queue's own train worktrees, and any user checkout — are never the target.
|
|
413
|
+
function targetWorktreeFor(cwd, target) {
|
|
414
|
+
const ref = targetRef(target);
|
|
415
|
+
return worktreeList(cwd).find((entry) => entry.branch === ref && !entry.bare && !entry.prunable) || null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// --- D-5: landing ------------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
function commitsBetween(cwd, from, to) {
|
|
421
|
+
const res = runGitRaw(cwd, ['log', '--format=%H', `${from}..${to}`]);
|
|
422
|
+
if (res.status !== 0) return [];
|
|
423
|
+
return res.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function mergeBase(cwd, a, b) {
|
|
427
|
+
const res = runGitRaw(cwd, ['merge-base', a, b]);
|
|
428
|
+
if (res.status !== 0) return null;
|
|
429
|
+
const value = res.stdout.trim();
|
|
430
|
+
return value || null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// D-5: fast-forward the target to exactly `train`. A target checked out somewhere goes through
|
|
434
|
+
// `git merge --ff-only` in that worktree (git refuses without modifying anything when local changes
|
|
435
|
+
// would be clobbered -> `target_dirty`); otherwise the branch is moved with a compare-and-swap.
|
|
436
|
+
//
|
|
437
|
+
// `expectedBase` is the commit this train was BUILT on, and it is what the swap compares against —
|
|
438
|
+
// never a re-read tip. Re-reading would move the target to a train that does not contain a commit
|
|
439
|
+
// that landed in the meantime, silently dropping it; comparing against the build base turns that
|
|
440
|
+
// race into `cas_lost`, which the caller treats exactly like a moved target (record the foreign
|
|
441
|
+
// commits, rebuild, retrain). A target checked out in a worktree cannot be rewound by
|
|
442
|
+
// `merge --ff-only` at all, but it can refuse; the HEAD comparison reports that as `cas_lost` too.
|
|
443
|
+
//
|
|
444
|
+
// WP-6: `opts.env` is handed to BOTH ref-moving git children, so the runner can present the
|
|
445
|
+
// `LAND_TOKEN` the E-1 reference-transaction guard compares against its own lease record. It is the
|
|
446
|
+
// env EXTRA, not a whole environment: `git.js#childEnv` starts from `process.env` minus the §4
|
|
447
|
+
// GIT_* list and only then applies `extra`, which is exactly how `runShell` passes `LAND_TOKEN` to
|
|
448
|
+
// gates (gate.js:151). Spreading `process.env` here instead would re-introduce the scrubbed GIT_*
|
|
449
|
+
// names (I-8). Existing callers pass no `opts` and are byte-for-byte unaffected.
|
|
450
|
+
function landTrain(cwd, target, train, expectedBase, opts = {}) {
|
|
451
|
+
const gitOpts = { env: opts.env };
|
|
452
|
+
const worktree = targetWorktreeFor(cwd, target);
|
|
453
|
+
if (worktree) {
|
|
454
|
+
const head = revParseOrNull(worktree.path, 'HEAD');
|
|
455
|
+
if (expectedBase !== undefined && head !== expectedBase) {
|
|
456
|
+
throw landingError('cas_lost', `${targetRef(target)} is checked out at ${head}, expected ${expectedBase}`, {
|
|
457
|
+
ref: targetRef(target), expected: expectedBase, current: head, cwd: worktree.path, worktree: worktree.path,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
const result = ffCheckedOut(worktree.path, train, gitOpts);
|
|
461
|
+
return { via: 'ff', worktree: worktree.path, sha: result.sha };
|
|
462
|
+
}
|
|
463
|
+
const expected = expectedBase === undefined ? revParseOrNull(cwd, targetRef(target)) : expectedBase;
|
|
464
|
+
casUpdateRef(cwd, targetRef(target), train, expected, gitOpts);
|
|
465
|
+
return { via: 'cas', worktree: null, sha: revParseOrNull(cwd, targetRef(target)) };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// --- D-8: conflicts confined to regenerate paths ------------------------------------------------
|
|
469
|
+
|
|
470
|
+
function changedFilesBetween(cwd, from, to) {
|
|
471
|
+
const res = runGitRaw(cwd, ['diff', '--name-only', from, to]);
|
|
472
|
+
if (res.status !== 0) return [];
|
|
473
|
+
return res.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function hunkHeaders(cwd, from, to, file) {
|
|
477
|
+
const res = runGitRaw(cwd, ['diff', '--no-color', '-U0', from, to, '--', file]);
|
|
478
|
+
if (res.status !== 0) return [];
|
|
479
|
+
return res.stdout.split('\n').filter((line) => line.startsWith('@@')).map((line) => line.trim());
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// D-9 Tier A, run per job on a FAIL: the files the target changed since the merge base intersect
|
|
483
|
+
// the files the branch changed. `sameHunkContext` marks a file where both sides moved the SAME
|
|
484
|
+
// -U0 hunk header (git's own function context), which is the strongest available signal short of a
|
|
485
|
+
// second gate — and a second gate is explicitly out of scope.
|
|
486
|
+
function overlapWithTarget(cwd, targetTip, tip) {
|
|
487
|
+
const base = mergeBase(cwd, targetTip, tip);
|
|
488
|
+
if (!base) return [];
|
|
489
|
+
const branchFiles = new Set(changedFilesBetween(cwd, base, tip));
|
|
490
|
+
const overlap = [];
|
|
491
|
+
for (const file of changedFilesBetween(cwd, base, targetTip)) {
|
|
492
|
+
if (!branchFiles.has(file)) continue;
|
|
493
|
+
const branchHunks = new Set(hunkHeaders(cwd, base, tip, file));
|
|
494
|
+
const shared = hunkHeaders(cwd, base, targetTip, file).some((header) => branchHunks.has(header));
|
|
495
|
+
overlap.push({ file, sameHunkContext: shared });
|
|
496
|
+
}
|
|
497
|
+
return overlap;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// The body shared by `regenerateTrain` (D-8) and `refreshTrain` (D-2): run every entry in the
|
|
501
|
+
// ALREADY-MATERIALIZED train worktree `dir` with the §4 LAND_* contract, honour the stop signal,
|
|
502
|
+
// then stage EXACTLY the paths the entries own and refuse to go on while anything is left unmerged.
|
|
503
|
+
// Returns the owned paths that actually changed, which is what tells `refreshTrain` whether the train
|
|
504
|
+
// commit has to be remade at all.
|
|
505
|
+
//
|
|
506
|
+
// Entries run in order and the next one may depend on the previous one's output, so the loop is
|
|
507
|
+
// deliberately sequential. A stopped entry is a STOP, not a failure — `land.js` turns the `stopped`
|
|
508
|
+
// code into `canceled` at its stop checkpoint.
|
|
509
|
+
async function runEntriesInWorktree({ dir, job, base, entries, log, signal, label, conflicts }) {
|
|
510
|
+
const logFd = log ? fs.openSync(log, 'a') : null;
|
|
511
|
+
const write = (text) => { if (logFd !== null) fs.writeSync(logFd, `${text}\n`); };
|
|
512
|
+
try {
|
|
513
|
+
for (const entry of entries) {
|
|
514
|
+
write(`$ ${entry.run}`);
|
|
515
|
+
// eslint-disable-next-line no-await-in-loop
|
|
516
|
+
const res = await runShell(entry.run, {
|
|
517
|
+
cwd: dir,
|
|
518
|
+
extra: { LAND_TRAIN: dir, LAND_BASE: base, LAND_TIP: job.tip, LAND_JOB: job.id },
|
|
519
|
+
logFd,
|
|
520
|
+
signal,
|
|
521
|
+
});
|
|
522
|
+
if (res.aborted) {
|
|
523
|
+
throw landingError('stopped', `${label} entry ${JSON.stringify(entry.run)} stopped`, {
|
|
524
|
+
jobId: job.id, run: entry.run,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (res.timedOut) {
|
|
528
|
+
throw landingError(`${label}_timeout`, `${label} entry ${JSON.stringify(entry.run)} timed out`, {
|
|
529
|
+
jobId: job.id, run: entry.run, conflicts,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (res.status !== 0) {
|
|
533
|
+
throw landingError(`${label}_failed`, `${label} entry ${JSON.stringify(entry.run)} exited ${res.status}`, {
|
|
534
|
+
jobId: job.id, run: entry.run, status: res.status, signal: res.signal,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
write(`[${label}] ${entry.run}: ok`);
|
|
538
|
+
}
|
|
539
|
+
} finally {
|
|
540
|
+
if (logFd !== null) fs.closeSync(logFd);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Stage EXACTLY the paths the entries own: expand each pattern against what actually changed.
|
|
544
|
+
const statusRes = runGitRaw(dir, ['status', '--porcelain']);
|
|
545
|
+
const changed = statusRes.stdout
|
|
546
|
+
.split('\n')
|
|
547
|
+
.map((line) => line.trimEnd())
|
|
548
|
+
.filter((line) => line.length > 3)
|
|
549
|
+
.map((line) => {
|
|
550
|
+
const body = line.slice(3);
|
|
551
|
+
const renamed = body.includes(' -> ') ? body.split(' -> ').pop() : body;
|
|
552
|
+
return renamed.startsWith('"') && renamed.endsWith('"') ? renamed.slice(1, -1) : renamed;
|
|
553
|
+
})
|
|
554
|
+
.filter((file) => entries.some((entry) => entry.paths.some((pattern) => globMatch(pattern, file))));
|
|
555
|
+
if (changed.length) {
|
|
556
|
+
const addArgs = ['add', '-A', '--', ...changed];
|
|
557
|
+
const added = runGitRaw(dir, addArgs);
|
|
558
|
+
if (added.status !== 0) throw gitFailed(dir, addArgs, added);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const unmerged = runGitRaw(dir, ['diff', '--name-only', '--diff-filter=U']);
|
|
562
|
+
const stillUnmerged = unmerged.stdout.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
563
|
+
if (stillUnmerged.length) {
|
|
564
|
+
throw landingError('conflict_unresolved', `${label} left unmerged paths: ${stillUnmerged.join(', ')}`, {
|
|
565
|
+
jobId: job.id, paths: stillUnmerged, conflicts,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
return changed;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// D-8: materialize the train worktree at the target tip, merge the branch WITHOUT committing, run
|
|
572
|
+
// each matching `regenerate` entry, stage exactly the paths the entries own, verify nothing is left
|
|
573
|
+
// unmerged, and commit. This is one of two cases where the train commit is made in a worktree — and
|
|
574
|
+
// it is made with PLUMBING (`write-tree` + `commit-tree`), never `git commit`: a real commit fires
|
|
575
|
+
// the repository's own `pre-commit` hook (a ten-minute gate here) and demands a configured
|
|
576
|
+
// `user.name`, neither of which a queue that is merely rebuilding a conflicted file has any
|
|
577
|
+
// business triggering.
|
|
578
|
+
async function regenerateTrain({ cwd, dir, job, base, conflicts, entries, log, signal }) {
|
|
579
|
+
addTrainWorktree(cwd, dir, base, job.id);
|
|
580
|
+
const mergeArgs = ['merge', '--no-commit', '--no-ff', job.tip];
|
|
581
|
+
const merged = runGitRaw(dir, mergeArgs);
|
|
582
|
+
// exit 1 is the expected "conflict" outcome we are here to resolve; anything else is a real
|
|
583
|
+
// failure of the merge machinery.
|
|
584
|
+
if (merged.status !== 0 && merged.status !== 1) throw gitFailed(dir, mergeArgs, merged);
|
|
585
|
+
|
|
586
|
+
await runEntriesInWorktree({ dir, job, base, entries, log, signal, label: 'regenerate', conflicts });
|
|
587
|
+
|
|
588
|
+
// The merge already staged the result in the train worktree's own index, so the tree is simply
|
|
589
|
+
// what that index says; the commit object is built with `commit-tree` (no hooks, no identity).
|
|
590
|
+
const tree = git(dir, ['write-tree']).trim();
|
|
591
|
+
const train = commitTree(dir, tree, [base, job.tip], trainMessage(job));
|
|
592
|
+
if (log) fs.appendFileSync(log, `[regenerate] committed ${train} (tree ${tree})\n`);
|
|
593
|
+
return { kind: 'train', base, tree, train, conflicts: [], regenerated: conflicts };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// D-2: the refresh pass. Unlike `regenerate`, a refresh entry runs on EVERY non-unchanged train, not
|
|
597
|
+
// only on a conflict: a tree-derived pin (the closure policy's `sourceBindings` digests) is stale the
|
|
598
|
+
// moment either side touches a bound source, and there is no textual conflict to notice it.
|
|
599
|
+
//
|
|
600
|
+
// The worktree is materialized at the ALREADY-BUILT train commit (`built.train`) — no merge, no
|
|
601
|
+
// `--no-ff` — because the train already contains both sides. The train commit is remade with plumbing
|
|
602
|
+
// only when an owned path actually changed, so a refresh that changes nothing leaves the merge-tree
|
|
603
|
+
// train byte-for-byte as it was and D-3's cache key, `job.tree` and the CAS all still describe the
|
|
604
|
+
// tree the gate approved.
|
|
605
|
+
async function refreshTrain({ cwd, dir, job, built, entries, log, signal }) {
|
|
606
|
+
addTrainWorktree(cwd, dir, built.train, job.id);
|
|
607
|
+
const changed = await runEntriesInWorktree({ dir, job, base: built.base, entries, log, signal, label: 'refresh', conflicts: [] });
|
|
608
|
+
|
|
609
|
+
const staged = runGitRaw(dir, ['diff', '--cached', '--quiet']);
|
|
610
|
+
// 0 = the index still matches `built.train`'s tree: the entries were a no-op, so there is nothing
|
|
611
|
+
// to commit and the merge-tree train stands as it was. 1 = something is staged, which is the
|
|
612
|
+
// normal "a digest was stale" case.
|
|
613
|
+
if (staged.status !== 0 && staged.status !== 1) throw gitFailed(dir, ['diff', '--cached', '--quiet'], staged);
|
|
614
|
+
if (staged.status === 0) {
|
|
615
|
+
if (log) fs.appendFileSync(log, `[refresh] no owned path changed (${entries.length} entry/entries); train ${built.train} unchanged\n`);
|
|
616
|
+
return { ...built, refreshed: [] };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const tree = git(dir, ['write-tree']).trim();
|
|
620
|
+
const train = commitTree(dir, tree, [built.base, job.tip], trainMessage(job));
|
|
621
|
+
if (log) fs.appendFileSync(log, `[refresh] committed ${train} (tree ${tree})\n`);
|
|
622
|
+
return { kind: 'train', base: built.base, tree, train, conflicts: [], refreshed: changed };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// --- D-6/D-7: the base rerun (§9 S-11, WP-2) ----------------------------------------------------
|
|
626
|
+
|
|
627
|
+
// The exact shapes `bash bin/test-gate.sh` prints on failure, read from the real landing logs
|
|
628
|
+
// (<repo>/.git/landing/logs/ — job 01m60h92c4ntd29g line 257072, job
|
|
629
|
+
// 01m60zmdj2e1tczj lines 3155 and 5970):
|
|
630
|
+
//
|
|
631
|
+
// [test-suite] FAILED (1 file(s)):
|
|
632
|
+
// - tests-integration/host-socket-protocol.test.js: terminal attempt failed (EXIT-HANG …)
|
|
633
|
+
// # [run-hermetic] FAILED (2 file(s)):
|
|
634
|
+
// # - tests/x.test.js: terminal attempt failed
|
|
635
|
+
// [lint-parallel] FAIL node ../bin/test-timing-lint.js --project-root . --baseline …
|
|
636
|
+
//
|
|
637
|
+
// The block body is `- <path>: <reason>` rows (bin/test-runner-adapter.js:921-944) and the `# `
|
|
638
|
+
// prefix appears when the gate relays another runner's stdout; both are accepted. A path is kept
|
|
639
|
+
// only when it really ends in `.test.js`, so a `[test-gate] FAIL ctm-changed (exit 1, …)` phase
|
|
640
|
+
// line or a `[test-suite] FAIL (introduced 1, pre-existing 0)` attribution line is never mistaken
|
|
641
|
+
// for a file.
|
|
642
|
+
//
|
|
643
|
+
// A row's path is relative to the RUNNER's cwd, not the repository root: bin/test-gate.sh:1238/1242
|
|
644
|
+
// invokes each project's runner as `cd <project> && node <runner> <files>`, so the real rows above
|
|
645
|
+
// read `tests-integration/host-socket-protocol.test.js`, not `coding-task-manager/...`. The block's
|
|
646
|
+
// label is what says which project that was (bin/test-runner-adapter.js:850 — `test-suite` is ctm,
|
|
647
|
+
// `run-hermetic` is wall-e), so it is what turns a row back into a repo-root-relative path; a path
|
|
648
|
+
// that already carries a project directory is never prefixed twice, and a label with no project
|
|
649
|
+
// (a root phase, which already runs from the repository root) is left alone. Anything the parser
|
|
650
|
+
// cannot place is still returned — `rerunOnBase` resolves it against the base tree and skips what
|
|
651
|
+
// it cannot find. Order is log order with duplicates dropped. `lint` is true when a
|
|
652
|
+
// `[lint-parallel] FAIL` line is present (bin/lint-parallel.js:561) — lint failures are deterministic
|
|
653
|
+
// on the train tree and are never rerun on base (§5 D-6).
|
|
654
|
+
const FAILED_BLOCK = /^\s*#?\s*\[([\w-]+)\]\s+FAILED\s+\(\d+\s+file\(s\)\):\s*$/;
|
|
655
|
+
const FAILED_ROW = /^\s*#?\s*[-*]\s*(\S+\.test\.js)\s*(?::|$)/;
|
|
656
|
+
const FAIL_LINE = /^\s*#?\s*\[([\w-]+)\]\s+FAIL\s+(\S+\.test\.js)\s*$/;
|
|
657
|
+
const LINT_FAIL = /^\s*#?\s*\[lint-parallel\]\s+FAIL\b/;
|
|
658
|
+
// The runner labels whose paths are relative to a project directory, and that directory.
|
|
659
|
+
const LABEL_PROJECTS = Object.freeze({ 'test-suite': 'coding-task-manager', 'run-hermetic': 'wall-e' });
|
|
660
|
+
const PROJECT_DIRS = Object.freeze(['coding-task-manager', 'wall-e']);
|
|
661
|
+
|
|
662
|
+
function normalizeTestPath(raw, label) {
|
|
663
|
+
let file = String(raw).trim().replace(/^["']+|["']+$/g, '').replace(/\\/g, '/');
|
|
664
|
+
while (file.startsWith('./')) file = file.slice(2);
|
|
665
|
+
const project = LABEL_PROJECTS[label];
|
|
666
|
+
if (project && !PROJECT_DIRS.some((dir) => file === dir || file.startsWith(`${dir}/`))) {
|
|
667
|
+
file = `${project}/${file}`;
|
|
668
|
+
}
|
|
669
|
+
return file;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Returns `{ tests, lint, raw, sawBlock }`: `tests` are repo-root-relative (see above), `raw` the
|
|
673
|
+
// same files exactly as the log printed them (what the gate-once merge-delta shadow compares against
|
|
674
|
+
// its selection S — `merge-delta.failingFiles` is a wrapper over this parser so the runner's log
|
|
675
|
+
// format is read in ONE place), and `sawBlock` whether any `FAILED (N file(s)):` block was present
|
|
676
|
+
// at all (no block = "cannot say", never "nothing failed").
|
|
677
|
+
function failedFilesFromLog(logText) {
|
|
678
|
+
const tests = [];
|
|
679
|
+
const raw = [];
|
|
680
|
+
const seen = new Set();
|
|
681
|
+
const seenRaw = new Set();
|
|
682
|
+
let lint = false;
|
|
683
|
+
let sawBlock = false;
|
|
684
|
+
let blockLabel = null;
|
|
685
|
+
const collect = (rawFile, label) => {
|
|
686
|
+
const printed = String(rawFile).trim();
|
|
687
|
+
if (printed && !seenRaw.has(printed)) { seenRaw.add(printed); raw.push(printed); }
|
|
688
|
+
const file = normalizeTestPath(rawFile, label);
|
|
689
|
+
if (!file || seen.has(file)) return;
|
|
690
|
+
seen.add(file);
|
|
691
|
+
tests.push(file);
|
|
692
|
+
};
|
|
693
|
+
for (const line of String(logText === undefined || logText === null ? '' : logText).split('\n')) {
|
|
694
|
+
if (!lint && LINT_FAIL.test(line)) lint = true;
|
|
695
|
+
if (blockLabel !== null) {
|
|
696
|
+
const row = FAILED_ROW.exec(line);
|
|
697
|
+
if (row) {
|
|
698
|
+
collect(row[1], blockLabel);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
// A blank line or anything that is not another `- path` row ends the block.
|
|
702
|
+
blockLabel = null;
|
|
703
|
+
}
|
|
704
|
+
const block = FAILED_BLOCK.exec(line);
|
|
705
|
+
if (block) {
|
|
706
|
+
blockLabel = block[1];
|
|
707
|
+
sawBlock = true;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const failed = FAIL_LINE.exec(line);
|
|
711
|
+
if (failed) collect(failed[2], failed[1]);
|
|
712
|
+
}
|
|
713
|
+
return { tests, lint, raw, sawBlock };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// The launchers a failed file can belong to, in a fixed order so a rerun is deterministic. A file is
|
|
717
|
+
// routed by its repo-root-relative prefix: `coding-task-manager/` and `wall-e/` are the two projects
|
|
718
|
+
// with their own runner (bin/test-gate.sh:1238/1242 invokes each as
|
|
719
|
+
// `cd <project> && node <runner> <project-relative files>`), and everything else is a root test run
|
|
720
|
+
// through the slot broker. `dir` is where the launcher runs, relative to the base worktree; `argv`
|
|
721
|
+
// builds its command line from project-relative files.
|
|
722
|
+
const RERUN_LAUNCHERS = Object.freeze([
|
|
723
|
+
{ id: 'coding-task-manager', dir: 'coding-task-manager', argv: (files) => ['bin/run-test-suite.js', ...files] },
|
|
724
|
+
{ id: 'wall-e', dir: 'wall-e', argv: (files) => ['tests/run-hermetic.js', ...files] },
|
|
725
|
+
{ id: 'root', dir: null, argv: (files) => ['bin/with-test-slots.js', '--', 'node', '--test', ...files] },
|
|
726
|
+
]);
|
|
727
|
+
|
|
728
|
+
function launcherFor(file) {
|
|
729
|
+
for (const launcher of RERUN_LAUNCHERS) {
|
|
730
|
+
if (launcher.dir && file.startsWith(`${launcher.dir}/`)) {
|
|
731
|
+
return { launcher, file: file.slice(launcher.dir.length + 1) };
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return { launcher: RERUN_LAUNCHERS[RERUN_LAUNCHERS.length - 1], file };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// A single-quoted POSIX word: the launcher command goes through `/bin/sh -c` (`runShell`), so a path
|
|
738
|
+
// with a space or a quote in it has to survive the shell.
|
|
739
|
+
function shQuote(value) {
|
|
740
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// The base worktree lives beside the gate's own train worktree, in the same trains root, so both are
|
|
744
|
+
// created and reaped by the same helpers and neither can be mistaken for somebody's checkout.
|
|
745
|
+
function baseWorktreeDir(cwd, jobId) {
|
|
746
|
+
return path.join(trainsRoot(cwd, commonDir(cwd)).dir, `land-${jobId}-base`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// D-6/D-7: rerun the files a FAILING train named on the BASE commit — Chromium's with-patch /
|
|
750
|
+
// without-patch rule: a failure that reproduces WITHOUT the branch is not the branch's failure.
|
|
751
|
+
//
|
|
752
|
+
// The rerun happens in a second worktree at `base` (same trains root and same cache symlinks as the
|
|
753
|
+
// gate's own), removed in the `finally` exactly like the gate's. Each project's launcher runs ONCE
|
|
754
|
+
// with only its own files, and a file counts as `failedOnBase` only when its launcher exits non-zero
|
|
755
|
+
// AND names that file through the same FAILED-block shape the gate prints — an exit code alone is
|
|
756
|
+
// not evidence about a specific file. A file that does not EXIST on base (a test the branch added)
|
|
757
|
+
// cannot be base-broken and is skipped, as is one whose launcher cannot run at all.
|
|
758
|
+
//
|
|
759
|
+
// The rerun output is appended to the job's gate log under a `[triage] rerun on base <sha12>` header,
|
|
760
|
+
// so one file holds both runs and `land inspect <job>` shows the whole diagnosis.
|
|
761
|
+
//
|
|
762
|
+
// `signal` is the job's AbortSignal: an abort stops the loop and returns what ran so far with
|
|
763
|
+
// `aborted: true` — the runner is being stopped, and a stopped diagnosis is never a verdict.
|
|
764
|
+
async function rerunOnBase(cwd, { base, files, root, jobId, signal, env, cachePaths = [], timeoutMs } = {}) {
|
|
765
|
+
const started = Date.now();
|
|
766
|
+
const ranFiles = [];
|
|
767
|
+
const named = new Set();
|
|
768
|
+
const dir = baseWorktreeDir(cwd, jobId);
|
|
769
|
+
const logFile = path.join(root, 'logs', `${jobId}.log`);
|
|
770
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'land-triage-'));
|
|
771
|
+
let aborted = false;
|
|
772
|
+
try {
|
|
773
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
774
|
+
fs.appendFileSync(logFile, `\n[triage] rerun on base ${String(base).slice(0, 12)}\n`);
|
|
775
|
+
addTrainWorktree(cwd, dir, base, jobId);
|
|
776
|
+
linkCachePaths(dir, cwd, cachePaths);
|
|
777
|
+
|
|
778
|
+
// Group by launcher in RERUN_LAUNCHERS order so the log reads the same way every time.
|
|
779
|
+
const groups = [];
|
|
780
|
+
for (const file of files) {
|
|
781
|
+
if (!fs.existsSync(path.join(dir, file))) continue; // added by the branch: base cannot be broken by it
|
|
782
|
+
const routed = launcherFor(file);
|
|
783
|
+
let group = groups.find((candidate) => candidate.id === routed.launcher.id);
|
|
784
|
+
if (!group) {
|
|
785
|
+
group = { id: routed.launcher.id, launcher: routed.launcher, files: [] };
|
|
786
|
+
groups.push(group);
|
|
787
|
+
}
|
|
788
|
+
group.files.push(routed.file);
|
|
789
|
+
ranFiles.push(file);
|
|
790
|
+
}
|
|
791
|
+
const rank = (group) => RERUN_LAUNCHERS.findIndex((launcher) => launcher.id === group.id);
|
|
792
|
+
groups.sort((a, b) => rank(a) - rank(b));
|
|
793
|
+
|
|
794
|
+
for (const group of groups) {
|
|
795
|
+
if (signal && signal.aborted) {
|
|
796
|
+
aborted = true;
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
const groupCwd = group.launcher.dir ? path.join(dir, group.launcher.dir) : dir;
|
|
800
|
+
const command = ['node', ...group.launcher.argv(group.files)].map(shQuote).join(' ');
|
|
801
|
+
fs.appendFileSync(logFile, `$ (cd ${group.launcher.dir || '.'} && ${command})\n`);
|
|
802
|
+
const outFile = path.join(scratch, `${group.id}.out`);
|
|
803
|
+
const outFd = fs.openSync(outFile, 'a');
|
|
804
|
+
let res;
|
|
805
|
+
try {
|
|
806
|
+
// eslint-disable-next-line no-await-in-loop -- launchers share the machine and run one at a time
|
|
807
|
+
res = await runShell(command, {
|
|
808
|
+
cwd: groupCwd,
|
|
809
|
+
extra: { ...(env || {}), LAND_JOB: jobId, LAND_BASE: base, LAND_TRIAGE: 'base' },
|
|
810
|
+
logFd: outFd,
|
|
811
|
+
// Bounded like the gate itself (the caller passes the gate's own timeout): a hanging test on
|
|
812
|
+
// base must not hold the runner lease past what the gate it is diagnosing was allowed.
|
|
813
|
+
timeoutMs,
|
|
814
|
+
signal,
|
|
815
|
+
});
|
|
816
|
+
} finally {
|
|
817
|
+
fs.closeSync(outFd);
|
|
818
|
+
}
|
|
819
|
+
const output = fs.readFileSync(outFile, 'utf8');
|
|
820
|
+
fs.appendFileSync(logFile, output);
|
|
821
|
+
if (res.aborted) {
|
|
822
|
+
aborted = true;
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
if (res.status === 0 || res.error) continue; // a launcher that cannot run proves nothing about base
|
|
826
|
+
const parsed = failedFilesFromLog(output);
|
|
827
|
+
for (const entry of parsed.tests) {
|
|
828
|
+
const file = group.launcher.dir && entry.startsWith(`${group.launcher.dir}/`)
|
|
829
|
+
? entry.slice(group.launcher.dir.length + 1)
|
|
830
|
+
: entry;
|
|
831
|
+
if (group.files.includes(file)) named.add(file);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
} finally {
|
|
835
|
+
removeTrainWorktree(cwd, dir);
|
|
836
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
837
|
+
}
|
|
838
|
+
const failedOnBase = files.filter((file) => {
|
|
839
|
+
const routed = launcherFor(file);
|
|
840
|
+
return named.has(routed.file) && ranFiles.includes(file);
|
|
841
|
+
});
|
|
842
|
+
return {
|
|
843
|
+
ranFiles,
|
|
844
|
+
failedOnBase,
|
|
845
|
+
aborted,
|
|
846
|
+
seconds: Number(((Date.now() - started) / 1000).toFixed(3)),
|
|
847
|
+
logPath: logFile,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
module.exports = {
|
|
852
|
+
MAX_ROUNDS,
|
|
853
|
+
GATE_KILL_GRACE_MS,
|
|
854
|
+
killGroup,
|
|
855
|
+
runShell,
|
|
856
|
+
targetRef,
|
|
857
|
+
globToRegExp,
|
|
858
|
+
globMatch,
|
|
859
|
+
matchingRegenerateEntries,
|
|
860
|
+
matchingRefreshEntries,
|
|
861
|
+
trainMessage,
|
|
862
|
+
buildTrain,
|
|
863
|
+
buildBatchTrain,
|
|
864
|
+
trainsRoot,
|
|
865
|
+
trainDirFor,
|
|
866
|
+
prepareTrainDir,
|
|
867
|
+
addTrainWorktree,
|
|
868
|
+
removeTrainWorktree,
|
|
869
|
+
worktreeList,
|
|
870
|
+
targetWorktreeFor,
|
|
871
|
+
commitsBetween,
|
|
872
|
+
mergeBase,
|
|
873
|
+
landTrain,
|
|
874
|
+
changedFilesBetween,
|
|
875
|
+
hunkHeaders,
|
|
876
|
+
overlapWithTarget,
|
|
877
|
+
regenerateTrain,
|
|
878
|
+
refreshTrain,
|
|
879
|
+
linkCachePaths,
|
|
880
|
+
failedFilesFromLog,
|
|
881
|
+
launcherFor,
|
|
882
|
+
shQuote,
|
|
883
|
+
baseWorktreeDir,
|
|
884
|
+
rerunOnBase,
|
|
885
|
+
};
|