@skillcap/gdh 0.22.0 → 0.24.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.
- package/INSTALL-BUNDLE.json +1 -1
- package/RELEASE-SPAN-UPDATE-CONTRACTS.json +129 -0
- package/node_modules/@gdh/adapters/dist/authoring-hook-render.d.ts.map +1 -1
- package/node_modules/@gdh/adapters/dist/authoring-hook-render.js +9 -2
- package/node_modules/@gdh/adapters/dist/authoring-hook-render.js.map +1 -1
- package/node_modules/@gdh/adapters/dist/claude-statusline-render.d.ts.map +1 -1
- package/node_modules/@gdh/adapters/dist/claude-statusline-render.js +2 -1
- package/node_modules/@gdh/adapters/dist/claude-statusline-render.js.map +1 -1
- package/node_modules/@gdh/adapters/dist/claude-update-hook-render.d.ts.map +1 -1
- package/node_modules/@gdh/adapters/dist/claude-update-hook-render.js +2 -1
- package/node_modules/@gdh/adapters/dist/claude-update-hook-render.js.map +1 -1
- package/node_modules/@gdh/adapters/dist/claude-update-worker-render.d.ts.map +1 -1
- package/node_modules/@gdh/adapters/dist/claude-update-worker-render.js +2 -1
- package/node_modules/@gdh/adapters/dist/claude-update-worker-render.js.map +1 -1
- package/node_modules/@gdh/adapters/dist/deferred-actions-advisory.d.ts +71 -0
- package/node_modules/@gdh/adapters/dist/deferred-actions-advisory.d.ts.map +1 -0
- package/node_modules/@gdh/adapters/dist/deferred-actions-advisory.js +89 -0
- package/node_modules/@gdh/adapters/dist/deferred-actions-advisory.js.map +1 -0
- package/node_modules/@gdh/adapters/dist/durable-backup.d.ts +209 -0
- package/node_modules/@gdh/adapters/dist/durable-backup.d.ts.map +1 -0
- package/node_modules/@gdh/adapters/dist/durable-backup.js +346 -0
- package/node_modules/@gdh/adapters/dist/durable-backup.js.map +1 -0
- package/node_modules/@gdh/adapters/dist/index.d.ts +10 -1
- package/node_modules/@gdh/adapters/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/adapters/dist/index.js +24 -2
- package/node_modules/@gdh/adapters/dist/index.js.map +1 -1
- package/node_modules/@gdh/adapters/dist/inventory-sweep.d.ts +53 -0
- package/node_modules/@gdh/adapters/dist/inventory-sweep.d.ts.map +1 -0
- package/node_modules/@gdh/adapters/dist/inventory-sweep.js +98 -0
- package/node_modules/@gdh/adapters/dist/inventory-sweep.js.map +1 -0
- package/node_modules/@gdh/adapters/dist/process-orchestration.d.ts +223 -0
- package/node_modules/@gdh/adapters/dist/process-orchestration.d.ts.map +1 -0
- package/node_modules/@gdh/adapters/dist/process-orchestration.js +368 -0
- package/node_modules/@gdh/adapters/dist/process-orchestration.js.map +1 -0
- package/node_modules/@gdh/adapters/dist/self-update-mechanics.d.ts +157 -14
- package/node_modules/@gdh/adapters/dist/self-update-mechanics.d.ts.map +1 -1
- package/node_modules/@gdh/adapters/dist/self-update-mechanics.js +570 -89
- package/node_modules/@gdh/adapters/dist/self-update-mechanics.js.map +1 -1
- package/node_modules/@gdh/adapters/dist/skill-rendering.d.ts.map +1 -1
- package/node_modules/@gdh/adapters/dist/skill-rendering.js +25 -0
- package/node_modules/@gdh/adapters/dist/skill-rendering.js.map +1 -1
- package/node_modules/@gdh/adapters/package.json +8 -8
- package/node_modules/@gdh/authoring/dist/diagnostics-broker-contract.d.ts +112 -0
- package/node_modules/@gdh/authoring/dist/diagnostics-broker-contract.d.ts.map +1 -0
- package/node_modules/@gdh/authoring/dist/diagnostics-broker-contract.js +264 -0
- package/node_modules/@gdh/authoring/dist/diagnostics-broker-contract.js.map +1 -0
- package/node_modules/@gdh/authoring/dist/diagnostics-broker.d.ts +92 -0
- package/node_modules/@gdh/authoring/dist/diagnostics-broker.d.ts.map +1 -0
- package/node_modules/@gdh/authoring/dist/diagnostics-broker.js +532 -0
- package/node_modules/@gdh/authoring/dist/diagnostics-broker.js.map +1 -0
- package/node_modules/@gdh/authoring/dist/index.d.ts +1 -0
- package/node_modules/@gdh/authoring/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/authoring/dist/index.js +1 -0
- package/node_modules/@gdh/authoring/dist/index.js.map +1 -1
- package/node_modules/@gdh/authoring/dist/lsp-client.d.ts +16 -0
- package/node_modules/@gdh/authoring/dist/lsp-client.d.ts.map +1 -1
- package/node_modules/@gdh/authoring/dist/lsp-client.js +24 -0
- package/node_modules/@gdh/authoring/dist/lsp-client.js.map +1 -1
- package/node_modules/@gdh/authoring/dist/lsp.d.ts.map +1 -1
- package/node_modules/@gdh/authoring/dist/lsp.js +123 -2
- package/node_modules/@gdh/authoring/dist/lsp.js.map +1 -1
- package/node_modules/@gdh/authoring/package.json +2 -2
- package/node_modules/@gdh/cli/dist/index.d.ts +9 -0
- package/node_modules/@gdh/cli/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/cli/dist/index.js +423 -5
- package/node_modules/@gdh/cli/dist/index.js.map +1 -1
- package/node_modules/@gdh/cli/dist/migrate.d.ts +152 -1
- package/node_modules/@gdh/cli/dist/migrate.d.ts.map +1 -1
- package/node_modules/@gdh/cli/dist/migrate.js +295 -9
- package/node_modules/@gdh/cli/dist/migrate.js.map +1 -1
- package/node_modules/@gdh/cli/dist/self-update.d.ts +14 -0
- package/node_modules/@gdh/cli/dist/self-update.d.ts.map +1 -1
- package/node_modules/@gdh/cli/dist/self-update.js +197 -15
- package/node_modules/@gdh/cli/dist/self-update.js.map +1 -1
- package/node_modules/@gdh/cli/package.json +10 -10
- package/node_modules/@gdh/core/dist/index.d.ts +209 -6
- package/node_modules/@gdh/core/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/core/dist/index.js +28 -5
- package/node_modules/@gdh/core/dist/index.js.map +1 -1
- package/node_modules/@gdh/core/dist/migrations/entries/s2c2_to_s2c3_rules_schema_v2_to_v3.d.ts +3 -0
- package/node_modules/@gdh/core/dist/migrations/entries/s2c2_to_s2c3_rules_schema_v2_to_v3.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/entries/s2c2_to_s2c3_rules_schema_v2_to_v3.js +247 -0
- package/node_modules/@gdh/core/dist/migrations/entries/s2c2_to_s2c3_rules_schema_v2_to_v3.js.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/entries/s3c8_to_s3c9_register_runtime_bridge_autoload.d.ts +3 -0
- package/node_modules/@gdh/core/dist/migrations/entries/s3c8_to_s3c9_register_runtime_bridge_autoload.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/entries/s3c8_to_s3c9_register_runtime_bridge_autoload.js +152 -0
- package/node_modules/@gdh/core/dist/migrations/entries/s3c8_to_s3c9_register_runtime_bridge_autoload.js.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/envelope-output-validator.d.ts +3 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/envelope-output-validator.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/envelope-output-validator.js +67 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/envelope-output-validator.js.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/index.d.ts +37 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/index.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/index.js +60 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/index.js.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/types.d.ts +121 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/types.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/types.js +2 -0
- package/node_modules/@gdh/core/dist/migrations/envelopes/types.js.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/golden-harness.d.ts +40 -0
- package/node_modules/@gdh/core/dist/migrations/golden-harness.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/golden-harness.js +71 -0
- package/node_modules/@gdh/core/dist/migrations/golden-harness.js.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/managed-surface-classes.d.ts +322 -0
- package/node_modules/@gdh/core/dist/migrations/managed-surface-classes.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/managed-surface-classes.js +384 -0
- package/node_modules/@gdh/core/dist/migrations/managed-surface-classes.js.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/probes.d.ts +58 -0
- package/node_modules/@gdh/core/dist/migrations/probes.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/probes.js +112 -0
- package/node_modules/@gdh/core/dist/migrations/probes.js.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/registry.d.ts +205 -0
- package/node_modules/@gdh/core/dist/migrations/registry.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/migrations/registry.js +214 -0
- package/node_modules/@gdh/core/dist/migrations/registry.js.map +1 -0
- package/node_modules/@gdh/core/dist/state/atomic-write.d.ts +19 -0
- package/node_modules/@gdh/core/dist/state/atomic-write.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/state/atomic-write.js +34 -0
- package/node_modules/@gdh/core/dist/state/atomic-write.js.map +1 -0
- package/node_modules/@gdh/core/dist/state/migration-state.d.ts +135 -0
- package/node_modules/@gdh/core/dist/state/migration-state.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/state/migration-state.js +186 -0
- package/node_modules/@gdh/core/dist/state/migration-state.js.map +1 -0
- package/node_modules/@gdh/core/dist/state/processes-snapshot.d.ts +72 -0
- package/node_modules/@gdh/core/dist/state/processes-snapshot.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/state/processes-snapshot.js +113 -0
- package/node_modules/@gdh/core/dist/state/processes-snapshot.js.map +1 -0
- package/node_modules/@gdh/core/dist/state/render-inventory.d.ts +54 -0
- package/node_modules/@gdh/core/dist/state/render-inventory.d.ts.map +1 -0
- package/node_modules/@gdh/core/dist/state/render-inventory.js +77 -0
- package/node_modules/@gdh/core/dist/state/render-inventory.js.map +1 -0
- package/node_modules/@gdh/core/package.json +1 -1
- package/node_modules/@gdh/docs/dist/agent-contract.d.ts.map +1 -1
- package/node_modules/@gdh/docs/dist/agent-contract.js +2 -1
- package/node_modules/@gdh/docs/dist/agent-contract.js.map +1 -1
- package/node_modules/@gdh/docs/dist/guidance.d.ts.map +1 -1
- package/node_modules/@gdh/docs/dist/guidance.js +14 -7
- package/node_modules/@gdh/docs/dist/guidance.js.map +1 -1
- package/node_modules/@gdh/docs/package.json +2 -2
- package/node_modules/@gdh/mcp/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/mcp/dist/index.js +77 -1
- package/node_modules/@gdh/mcp/dist/index.js.map +1 -1
- package/node_modules/@gdh/mcp/package.json +8 -8
- package/node_modules/@gdh/observability/package.json +2 -2
- package/node_modules/@gdh/runtime/dist/bridge-surface.js +63 -2
- package/node_modules/@gdh/runtime/dist/bridge-surface.js.map +1 -1
- package/node_modules/@gdh/runtime/package.json +2 -2
- package/node_modules/@gdh/scan/package.json +3 -3
- package/node_modules/@gdh/verify/package.json +7 -7
- package/package.json +11 -11
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { resolvePinnedVersion, resolvePinnedVersionOrNull, resolveProjectRoot, writePinnedVersion, } from "@gdh/authoring";
|
|
4
|
-
import { invalidateGdhUpdateCache, resolveEffectiveDevRepoPath } from "@gdh/core";
|
|
3
|
+
import { readProjectConfig, resolvePinnedVersion, resolvePinnedVersionOrNull, resolveProjectRoot, writePinnedVersion, } from "@gdh/authoring";
|
|
4
|
+
import { GDH_AGENT_CONTRACT_VERSION, GDH_PROJECT_CONFIG_VERSION, MIGRATION_REGISTRY_ENTRIES, applyMigrationChain, deleteProcessesSnapshot, invalidateGdhUpdateCache, readMigrationState, readProcessesSnapshot, readRenderInventory, resolveEffectiveDevRepoPath, writeMigrationState, writeProcessesSnapshot, writeRenderInventory, } from "@gdh/core";
|
|
5
|
+
import { detectClass1MarkerDrift, driftResultToPreservationNotes, readBackupManifest, restoreFromDurableBackup, writeDurableBackup, } from "./durable-backup.js";
|
|
6
|
+
import { runInventorySweep } from "./inventory-sweep.js";
|
|
7
|
+
import { captureProcessSnapshot, defaultProcessOrchestrationEnvironment, restartFromSnapshot, stopCapturedProcesses, } from "./process-orchestration.js";
|
|
5
8
|
// Dynamic import breaks the circular dependency with ./index.ts.
|
|
6
9
|
//
|
|
7
10
|
// self-update-mechanics.ts is re-exported from adapters/index.ts (the barrel).
|
|
@@ -18,20 +21,91 @@ async function loadInstallSupportedAgentAdapters() {
|
|
|
18
21
|
const { installSupportedAgentAdapters } = await import("./index.js");
|
|
19
22
|
return installSupportedAgentAdapters;
|
|
20
23
|
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Plan 72-08 — pair resolution helpers (D-10 step 2)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
21
27
|
/**
|
|
22
|
-
*
|
|
23
|
-
* `gdh_version` pin and re-bakes every managed surface at the new pin.
|
|
28
|
+
* Resolve the {schema, agentContract} pair for an onboarded target.
|
|
24
29
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
* Returns:
|
|
31
|
+
* - `null` when `.gdh/project.yaml` is absent or unparseable (project not
|
|
32
|
+
* onboarded; bumpAndRebakePin treats this as `blocked` with reason
|
|
33
|
+
* `current_pair_unresolvable`)
|
|
34
|
+
* - the supported-range floor pair `{schema: 2, agentContract: 2}` for any
|
|
35
|
+
* readable target
|
|
29
36
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
37
|
+
* Why a constant floor instead of the on-disk version? The supported floor
|
|
38
|
+
* pair gates the s2c2_to_s2c3 registry entry's range filter
|
|
39
|
+
* (entry.from = {2,2} must be ≥ fromPair). Per Plan 72-02 audit, every entry
|
|
40
|
+
* in the supported range is idempotent — running it on a target that's
|
|
41
|
+
* already past the entry's `to` pair is a verify-true no-op. Using the
|
|
42
|
+
* constant floor means the chain considers every entry on every run; the
|
|
43
|
+
* entry transforms self-detect "already at HEAD" and skip work, and the
|
|
44
|
+
* entry verify probes self-confirm "rules.yaml at version: 3" regardless of
|
|
45
|
+
* whether the entry just rewrote it or it was already there.
|
|
32
46
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
47
|
+
* The on-disk schema version (`version:` field of `.gdh/project.yaml`) is
|
|
48
|
+
* read defensively to confirm the config is parseable, but is not used to
|
|
49
|
+
* narrow the pair: per the audit, project.yaml `version: 3` has been
|
|
50
|
+
* forward-compatible since v0.18 and does not gate registry entries.
|
|
51
|
+
*/
|
|
52
|
+
export async function resolveCurrentMigrationPairOrNull(targetPath) {
|
|
53
|
+
const configPath = path.join(targetPath, ".gdh/project.yaml");
|
|
54
|
+
const content = await fs.readFile(configPath, "utf8").catch(() => null);
|
|
55
|
+
if (content === null)
|
|
56
|
+
return null;
|
|
57
|
+
// Defensive parse: ensure the file has at minimum a recognizable schema
|
|
58
|
+
// header. If parse fails, treat as unresolvable.
|
|
59
|
+
const firstLine = content.split("\n", 1)[0] ?? "";
|
|
60
|
+
if (!/^version:\s*\d+/.test(firstLine))
|
|
61
|
+
return null;
|
|
62
|
+
return { schema: 2, agentContract: 2 };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the {schema, agentContract} pair for the running GDH version.
|
|
66
|
+
*
|
|
67
|
+
* Always the package's compile-time pair: `GDH_PROJECT_CONFIG_VERSION` and
|
|
68
|
+
* `GDH_AGENT_CONTRACT_VERSION` from `@gdh/core`. This is the chain's `to`
|
|
69
|
+
* pair ceiling — applyMigrationChain runs every entry whose `to` ≤ this pair.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveTargetMigrationPair() {
|
|
72
|
+
return {
|
|
73
|
+
schema: GDH_PROJECT_CONFIG_VERSION,
|
|
74
|
+
agentContract: GDH_AGENT_CONTRACT_VERSION,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Phase 12 MIG-01 + MIG-02 + Plan 72-08 D-10 orchestrator. Atomically bumps the
|
|
79
|
+
* `.gdh/project.yaml` `gdh_version` pin, re-bakes every managed surface at the
|
|
80
|
+
* new pin (class-1), runs the migration registry chain (class-2/3 transforms +
|
|
81
|
+
* verify probes), sweeps orphan files, and orchestrates managed-process
|
|
82
|
+
* stop/restart with a deferred-action recovery path on restart failure.
|
|
83
|
+
*
|
|
84
|
+
* The full 12-step ordering (D-10):
|
|
85
|
+
* 1. SELF-03 dev-mode short-circuit (preserved unchanged)
|
|
86
|
+
* 2. Plan: dry-run installSupportedAgentAdapters + resolve from/to pair
|
|
87
|
+
* 3. Capture process snapshot (writeProcessesSnapshot)
|
|
88
|
+
* 4. Stop processes (broker → pruneBroker; mcp → graceful → hard kill)
|
|
89
|
+
* 5. Write durable backup (.gdh-state/backup/) with WFL-02 marker drift detection
|
|
90
|
+
* 6. Write pin (writePinnedVersion) — MIG-02 sequencing precedes final re-bake
|
|
91
|
+
* 7. Re-bake adapters (installSupportedAgentAdapters apply)
|
|
92
|
+
* 8. Apply migration chain (registry entries class-2/3); failure → 8a rollback
|
|
93
|
+
* 9. Inventory set-diff sweep (orphan deletion)
|
|
94
|
+
* 10. Write new render-inventory.json + migration.json
|
|
95
|
+
* 11. Restart processes from snapshot; on all_restarted delete snapshot,
|
|
96
|
+
* on some_failed write process_restart deferred action (D-19)
|
|
97
|
+
* 12. Emit structured BumpAndRebakePinResult
|
|
98
|
+
*
|
|
99
|
+
* On step-8 chain failure: restoreFromDurableBackup re-creates files byte-equal
|
|
100
|
+
* pre-update (writePinnedVersion is rolled back automatically because
|
|
101
|
+
* `.gdh/project.yaml` is in the captured paths). Processes stay stopped per
|
|
102
|
+
* WFL-01 D-18 step 6. Inventory is NOT updated (the new render set was never
|
|
103
|
+
* reached coherently). Result: `rolled_back_from_durable_backup`.
|
|
104
|
+
*
|
|
105
|
+
* Sequencing invariant (MIG-02 + validate-docs Check 43): writePinnedVersion
|
|
106
|
+
* is called textually BEFORE the FINAL installSupportedAgentAdapters call in
|
|
107
|
+
* this file. The dry-run plan call at step 2 reads the current pin; the apply
|
|
108
|
+
* call at step 7 reads the new pin from disk after writePinnedVersion.
|
|
35
109
|
*/
|
|
36
110
|
export async function bumpAndRebakePin(targetPath, options) {
|
|
37
111
|
const rawTargetPath = path.resolve(targetPath);
|
|
@@ -46,23 +120,74 @@ export async function bumpAndRebakePin(targetPath, options) {
|
|
|
46
120
|
currentVersion: await resolvePinnedVersionOrNull(effectiveTargetPath),
|
|
47
121
|
};
|
|
48
122
|
}
|
|
49
|
-
// Step 2
|
|
123
|
+
// Step 2 (plan + pair resolution).
|
|
124
|
+
const fromPair = await resolveCurrentMigrationPairOrNull(effectiveTargetPath);
|
|
125
|
+
if (fromPair === null) {
|
|
126
|
+
return {
|
|
127
|
+
state: "blocked",
|
|
128
|
+
reason: "current_pair_unresolvable: .gdh/project.yaml is missing or unparseable",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const toPair = resolveTargetMigrationPair();
|
|
132
|
+
// resolvePinnedVersion throws with the gdh-migrate-apply hint if the pin
|
|
133
|
+
// line is missing — but resolveCurrentMigrationPairOrNull already gated
|
|
134
|
+
// the absence-of-config case. Pin-line absence after that is genuine.
|
|
50
135
|
const fromVersion = await resolvePinnedVersion(effectiveTargetPath);
|
|
51
|
-
|
|
136
|
+
// ── Phase 73 D-11 — resume detection ──
|
|
137
|
+
// Read migration.json once for the resume marker. If a marker exists and
|
|
138
|
+
// its `to_pin` matches options.targetVersion, treat this run as a resume of
|
|
139
|
+
// a previously-paused chain: skip the noop early-return AND skip steps 3-7
|
|
140
|
+
// (snapshot capture, stop, durable backup, pin write, re-bake) since they
|
|
141
|
+
// already ran in the original attempt. Re-running steps 5-7 would either be
|
|
142
|
+
// a no-op (idempotent pin write, idempotent re-bake of already-baked
|
|
143
|
+
// surfaces) OR destructive (overwriting the durable backup with post-pause
|
|
144
|
+
// content, which would defeat the all-or-nothing rollback contract). The
|
|
145
|
+
// marker check MUST run BEFORE the `fromVersion === options.targetVersion`
|
|
146
|
+
// early-return below, because on resume the pin has already been written
|
|
147
|
+
// (step 6 of the original attempt) and would otherwise short-circuit as
|
|
148
|
+
// noop.
|
|
149
|
+
const priorState = await readMigrationState(effectiveTargetPath);
|
|
150
|
+
const resumeMarker = priorState?.pending_envelope_resume ?? null;
|
|
151
|
+
const wasResume = resumeMarker !== null && resumeMarker.to_pin === options.targetVersion;
|
|
152
|
+
// Defensive: marker present but for a different target (e.g., manual
|
|
153
|
+
// downgrade or hand-edited migration.json with mismatched to_pin). Clear
|
|
154
|
+
// the stale marker and fall through to the standard pin-equality check —
|
|
155
|
+
// the run continues as a fresh self-update attempt. Mitigates T-73-04-01.
|
|
156
|
+
if (resumeMarker !== null && !wasResume) {
|
|
157
|
+
// exactOptionalPropertyTypes: cannot assign undefined; rebuild without
|
|
158
|
+
// the marker field. envelopes carries forward as-is (recorded-result
|
|
159
|
+
// evidence is independent of the in-flight marker).
|
|
160
|
+
const cleared = {
|
|
161
|
+
migration_version: 1,
|
|
162
|
+
last_applied_at: priorState.last_applied_at,
|
|
163
|
+
deferred_actions: priorState.deferred_actions,
|
|
164
|
+
...(priorState.envelopes !== undefined
|
|
165
|
+
? { envelopes: priorState.envelopes }
|
|
166
|
+
: {}),
|
|
167
|
+
};
|
|
168
|
+
await writeMigrationState(effectiveTargetPath, cleared);
|
|
169
|
+
}
|
|
170
|
+
if (!wasResume && fromVersion === options.targetVersion) {
|
|
52
171
|
return { state: "noop", currentVersion: fromVersion };
|
|
53
172
|
}
|
|
54
|
-
// Step 3: Plan — dry-run to collect actions. File SET is determined by adapter
|
|
55
|
-
// status + guidance readiness, not by pin value; planning against current pin
|
|
56
|
-
// tells us which files to capture. Per research recommendation (c).
|
|
57
173
|
const installSupportedAgentAdapters = await loadInstallSupportedAgentAdapters();
|
|
58
174
|
const plan = await installSupportedAgentAdapters(effectiveTargetPath, {
|
|
59
175
|
dryRun: true,
|
|
60
176
|
allowBootstrap: true,
|
|
61
177
|
});
|
|
62
|
-
// Step 4: Capture originals (including .gdh/project.yaml) before any mutation.
|
|
63
|
-
const configPath = path.join(effectiveTargetPath, ".gdh/project.yaml");
|
|
64
|
-
const originals = await captureOriginals(plan.actions, configPath);
|
|
65
178
|
if (options.dryRun === true) {
|
|
179
|
+
// WR-04: when the chain is paused on an envelope (resumeMarker is set and
|
|
180
|
+
// wasResume is true), a dry-run cannot truthfully report `updated`:
|
|
181
|
+
// pending_envelope_resume is still on disk and gdh status will continue to
|
|
182
|
+
// emit the envelope-pending advisory. Surface as `blocked` so the agent
|
|
183
|
+
// sees the real next action — record the envelope result and re-run
|
|
184
|
+
// without --dry-run.
|
|
185
|
+
if (wasResume && resumeMarker !== null) {
|
|
186
|
+
return {
|
|
187
|
+
state: "blocked",
|
|
188
|
+
reason: `dry_run_during_envelope_pause: chain is paused on envelope_ref="${resumeMarker.envelope_ref}"; record the result and re-run without --dry-run`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
66
191
|
return {
|
|
67
192
|
state: "updated",
|
|
68
193
|
fromVersion,
|
|
@@ -70,95 +195,451 @@ export async function bumpAndRebakePin(targetPath, options) {
|
|
|
70
195
|
actions: plan.actions,
|
|
71
196
|
};
|
|
72
197
|
}
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
198
|
+
const env = options.processOrchestrationEnvironment ?? defaultProcessOrchestrationEnvironment();
|
|
199
|
+
// Hoisted across the wasResume branch so step 11 (restart) and step 12
|
|
200
|
+
// (emit report) can read them on both fresh and resume paths.
|
|
201
|
+
let preservationNotes = [];
|
|
202
|
+
let rebakeActions = [];
|
|
203
|
+
// captureResult/stopResult are intentionally null on resume; step 11 reads
|
|
204
|
+
// the snapshot via readProcessesSnapshot(effectiveTargetPath) instead.
|
|
205
|
+
let captureResult = null;
|
|
206
|
+
let stopResult = null;
|
|
207
|
+
// Computed inside the !wasResume branch (steps 5/9/10 reference it). On
|
|
208
|
+
// resume, step 9 has already run in the original attempt — but we still
|
|
209
|
+
// re-derive renderedRelativePaths from the planned actions before writing
|
|
210
|
+
// render-inventory.json and migration.json so success-write parity is
|
|
211
|
+
// preserved.
|
|
212
|
+
let allPlannedRelativePaths = [];
|
|
213
|
+
if (wasResume) {
|
|
214
|
+
// Steps 3-7 already ran in the original attempt:
|
|
215
|
+
// - Step 3 (capture snapshot) — processes-snapshot.json persists on disk
|
|
216
|
+
// - Step 4 (stop) — stop_method-per-row recorded in that snapshot
|
|
217
|
+
// - Step 5 (drift detect + durable backup) — backup persists; manifest
|
|
218
|
+
// carries `preservation_notes` from the original drift detection
|
|
219
|
+
// - Step 6 (pin write) — pin already at options.targetVersion (so the
|
|
220
|
+
// noop early-return above had to be guarded by !wasResume). Even if
|
|
221
|
+
// re-invoked, writePinnedVersion is idempotent for the same target.
|
|
222
|
+
// - Step 7 (re-bake) — surfaces are already re-baked
|
|
223
|
+
// Re-taking the durable backup here would clobber the pre-update content
|
|
224
|
+
// captured originally — defeating the rollback substrate (T-73-04-02).
|
|
225
|
+
//
|
|
226
|
+
// Load preservation_notes from the durable backup manifest written in the
|
|
227
|
+
// original step 5. If the manifest is unreadable (missing or schema-
|
|
228
|
+
// mismatched), fall back to empty notes — the agent already saw the
|
|
229
|
+
// original report when the chain paused, so this is informational only.
|
|
230
|
+
const manifest = await readBackupManifest(effectiveTargetPath);
|
|
231
|
+
preservationNotes = manifest?.preservation_notes ?? [];
|
|
232
|
+
// Also re-derive the planned action paths so steps 9 and 10 (inventory
|
|
233
|
+
// sweep + render-inventory write) see the same surfaces the original
|
|
234
|
+
// attempt computed.
|
|
235
|
+
allPlannedRelativePaths = computePlannedRelativePaths(plan.actions, effectiveTargetPath);
|
|
79
236
|
}
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const rebake = await installSupportedAgentAdapters(effectiveTargetPath, {
|
|
87
|
-
allowBootstrap: true,
|
|
237
|
+
else {
|
|
238
|
+
// Step 3: Capture process snapshot.
|
|
239
|
+
captureResult = await captureProcessSnapshot({
|
|
240
|
+
targetPath: effectiveTargetPath,
|
|
241
|
+
env,
|
|
88
242
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
243
|
+
// WR-01: Snapshot write is the recovery anchor for the stop step. If we
|
|
244
|
+
// cannot persist the captured snapshot, do NOT proceed to stop processes —
|
|
245
|
+
// a SIGTERM with no recorded snapshot leaves the operator with stopped
|
|
246
|
+
// processes and no rows to drive restart. Convert to blocked so the
|
|
247
|
+
// structured BumpAndRebakePinResult shape is preserved through the CLI.
|
|
248
|
+
try {
|
|
249
|
+
await writeProcessesSnapshot(effectiveTargetPath, captureResult.snapshot);
|
|
94
250
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
await
|
|
251
|
+
catch (error) {
|
|
252
|
+
return {
|
|
253
|
+
state: "blocked",
|
|
254
|
+
reason: `processes_snapshot_write_failed: ${formatError(error)}`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// Step 4: Stop processes; persist updated snapshot (with stop_method per row).
|
|
258
|
+
stopResult = await stopCapturedProcesses({
|
|
259
|
+
snapshot: captureResult.snapshot,
|
|
260
|
+
env,
|
|
261
|
+
targetPath: effectiveTargetPath,
|
|
262
|
+
});
|
|
263
|
+
// WR-01: Stop has already happened — by the time we get here processes are
|
|
264
|
+
// SIGTERM'd. A failed snapshot write at this point is still recoverable
|
|
265
|
+
// contractually (the captured snapshot from step 3 is on disk) but we
|
|
266
|
+
// surface the failure as blocked so downstream consumers (D-15/D-18) see
|
|
267
|
+
// the structured shape rather than a thrown exception.
|
|
268
|
+
try {
|
|
269
|
+
await writeProcessesSnapshot(effectiveTargetPath, stopResult.updatedSnapshot);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
return {
|
|
273
|
+
state: "blocked",
|
|
274
|
+
reason: `processes_snapshot_write_failed: ${formatError(error)}`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// Step 5: WFL-02 drift detection + durable backup.
|
|
278
|
+
allPlannedRelativePaths = computePlannedRelativePaths(plan.actions, effectiveTargetPath);
|
|
279
|
+
// Approximate class-1 set: the plan's actions include skill rendering
|
|
280
|
+
// (per-target paths under .claude/.codex/.cursor + .gdh/runtime-knowledge)
|
|
281
|
+
// which are deterministic class-1 surfaces. We feed every plan-derived
|
|
282
|
+
// relative path through detectClass1MarkerDrift; KNOWN_GDH_MARKERS only
|
|
283
|
+
// matches files that GDH actually rendered (those carry the marker), so
|
|
284
|
+
// false positives degrade to manifest entries — never silent loss.
|
|
285
|
+
const driftResult = await detectClass1MarkerDrift({
|
|
286
|
+
targetPath: effectiveTargetPath,
|
|
287
|
+
plannedClass1Paths: allPlannedRelativePaths,
|
|
288
|
+
});
|
|
289
|
+
preservationNotes = driftResultToPreservationNotes(driftResult);
|
|
290
|
+
const allPlannedPaths = uniquePaths([
|
|
291
|
+
".gdh/project.yaml",
|
|
292
|
+
...allPlannedRelativePaths,
|
|
293
|
+
]);
|
|
294
|
+
const fromPin = (await resolvePinnedVersionOrNull(effectiveTargetPath)) ?? "";
|
|
295
|
+
// WR-03: defensive cross-check before clobbering the durable backup. If we
|
|
296
|
+
// are on the fresh-run path (!wasResume), no resume marker is set, but a
|
|
297
|
+
// backup manifest already exists, the most likely explanation is that
|
|
298
|
+
// migration.json was deleted (or hand-edited) between a paused chain and
|
|
299
|
+
// this run. Overwriting the existing backup with post-pause content would
|
|
300
|
+
// destroy the rollback substrate (T-73-04-02). Surface as blocked so the
|
|
301
|
+
// operator can run `gdh migration clear-backups` after confirming state.
|
|
302
|
+
const existingManifest = await readBackupManifest(effectiveTargetPath);
|
|
303
|
+
if (existingManifest !== null) {
|
|
304
|
+
return {
|
|
305
|
+
state: "blocked",
|
|
306
|
+
reason: "stale_durable_backup_no_marker: a durable backup exists but migration.json has no pending_envelope_resume marker; run `gdh migration clear-backups` after confirming the workspace state",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
await writeDurableBackup({
|
|
310
|
+
targetPath: effectiveTargetPath,
|
|
311
|
+
fromPair,
|
|
312
|
+
toPair,
|
|
313
|
+
fromPin,
|
|
314
|
+
toPin: options.targetVersion,
|
|
315
|
+
plannedPaths: allPlannedPaths,
|
|
316
|
+
preservationNotes,
|
|
317
|
+
});
|
|
318
|
+
// Step 6: COMMIT POINT — write new pin first (MIG-02 sequencing; Check 43).
|
|
319
|
+
try {
|
|
320
|
+
await writePinnedVersion(effectiveTargetPath, options.targetVersion);
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
// Pin write failed before the chain ran. Backup is intact; restore so
|
|
324
|
+
// .gdh/project.yaml is unchanged and the operator can re-run.
|
|
325
|
+
await restoreFromDurableBackup({ targetPath: effectiveTargetPath });
|
|
326
|
+
return {
|
|
327
|
+
state: "blocked",
|
|
328
|
+
reason: `Pin write failed: ${formatError(error)}`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// Step 7: Re-bake adapters (installSupportedAgentAdapters reads the new pin).
|
|
332
|
+
try {
|
|
333
|
+
const rebake = await installSupportedAgentAdapters(effectiveTargetPath, {
|
|
334
|
+
allowBootstrap: true,
|
|
335
|
+
});
|
|
336
|
+
rebakeActions = rebake.actions;
|
|
337
|
+
const blocked = rebake.actions.filter((a) => a.state === "blocked");
|
|
338
|
+
if (blocked.length > 0) {
|
|
339
|
+
throw new Error(`Re-bake had ${blocked.length} blocked action(s): ${blocked
|
|
340
|
+
.map((a) => a.summary)
|
|
341
|
+
.join("; ")}`);
|
|
342
|
+
}
|
|
343
|
+
// Step 7b: Repair the runtime bridge surface. Without this, self-update
|
|
344
|
+
// re-bakes the managed adapters and addon files but leaves project.godot
|
|
345
|
+
// missing the GDHBridge autoload, which `inspectProjectLifecycleCompatibility`
|
|
346
|
+
// surfaces as `compatibility_degraded` with reasons
|
|
347
|
+
// `runtime_bridge_plugin_enable_required` /
|
|
348
|
+
// `runtime_bridge_autoload_missing_or_drifted`. The repair pass runs the
|
|
349
|
+
// v1.18 register_autoload class-3 op deterministically (see
|
|
350
|
+
// GDH_MANAGED_SURFACE_CLASSES `project_godot.allowedOps`), so the chain
|
|
351
|
+
// matrix can prove "every supported version reaches HEAD via gdh
|
|
352
|
+
// self-update without manual intervention".
|
|
353
|
+
const projectConfigForBridge = await readProjectConfig(effectiveTargetPath);
|
|
354
|
+
if (projectConfigForBridge !== null) {
|
|
355
|
+
const { repairRuntimeBridgeSurface } = await import("@gdh/runtime");
|
|
356
|
+
await repairRuntimeBridgeSurface({
|
|
357
|
+
targetPath: effectiveTargetPath,
|
|
358
|
+
projectConfig: projectConfigForBridge,
|
|
359
|
+
dryRun: false,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// Phase 16 UPD-05 Fix B: invalidate the shared update-check cache so the
|
|
363
|
+
// Claude statusline clears its update indicator in the same session.
|
|
364
|
+
await invalidateGdhUpdateCache();
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
// Re-bake failure is treated as a chain-equivalent rollback path: restore
|
|
368
|
+
// from durable backup so pin + class-1 surfaces revert atomically.
|
|
369
|
+
const restore = await restoreFromDurableBackup({
|
|
370
|
+
targetPath: effectiveTargetPath,
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
state: "rolled_back_from_durable_backup",
|
|
374
|
+
// WR-03: surface the resolved fromVersion (line 266) — never `fromPin`,
|
|
375
|
+
// which uses the OrNull `?? ""` fallback and would silently coerce a
|
|
376
|
+
// contractually-impossible null into "" and violate the typed
|
|
377
|
+
// `BumpAndRebakePinResult.fromVersion: string` semver invariant.
|
|
378
|
+
fromVersion,
|
|
379
|
+
attemptedToVersion: options.targetVersion,
|
|
380
|
+
failureReason: {
|
|
381
|
+
kind: "transform_threw",
|
|
382
|
+
entryId: "rebake_pre_chain",
|
|
383
|
+
error: formatError(error),
|
|
384
|
+
},
|
|
385
|
+
preservationNotes,
|
|
386
|
+
restoredPaths: restore.state === "restored" ? restore.restored : [],
|
|
387
|
+
removedPaths: restore.state === "restored" ? restore.removed : [],
|
|
388
|
+
// WR-06: surface aggregate restore-failure count so callers don't
|
|
389
|
+
// mistake a totally-failed restore for a clean rollback.
|
|
390
|
+
restoreFailedCount: restore.state === "restored" ? restore.failedCount : 0,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Step 8: Apply migration chain. Pause on needs_envelope (Phase 73 D-09).
|
|
395
|
+
// Verify/transform failure → 8a rollback.
|
|
396
|
+
const chainResult = await applyMigrationChain(effectiveTargetPath, fromPair, toPair, undefined, // entries — default to MIGRATION_REGISTRY_ENTRIES
|
|
397
|
+
// Plan 73-03 envelopePinContext: pin the chain's D-10 staleness check to
|
|
398
|
+
// this bump's pair. Production callers always pass this so the gate
|
|
399
|
+
// doesn't fail closed unconditionally.
|
|
400
|
+
{ fromPin: fromVersion, toPin: options.targetVersion });
|
|
401
|
+
// Phase 73 D-11 — chain paused on an unresolved envelope_ref. Atomically
|
|
402
|
+
// write the in-flight resume marker (mirrors WR-02 receipt-before-cleanup:
|
|
403
|
+
// the marker is the resume substrate, distinct from the migration.json
|
|
404
|
+
// success receipt). Read fresh state to preserve any existing envelopes
|
|
405
|
+
// slot history (D-08) and deferred actions across the marker write.
|
|
406
|
+
if (chainResult.state === "needs_envelope") {
|
|
407
|
+
const currentState = await readMigrationState(effectiveTargetPath);
|
|
408
|
+
const pauseState = {
|
|
409
|
+
migration_version: 1,
|
|
410
|
+
last_applied_at: currentState?.last_applied_at ?? {
|
|
411
|
+
package: fromVersion,
|
|
412
|
+
schema: fromPair.schema,
|
|
413
|
+
agentContract: fromPair.agentContract,
|
|
414
|
+
},
|
|
415
|
+
deferred_actions: currentState?.deferred_actions ?? [],
|
|
416
|
+
...(currentState?.envelopes !== undefined
|
|
417
|
+
? { envelopes: currentState.envelopes }
|
|
418
|
+
: {}),
|
|
419
|
+
pending_envelope_resume: {
|
|
420
|
+
envelope_ref: chainResult.envelopeRef,
|
|
421
|
+
from_pin: fromVersion,
|
|
422
|
+
to_pin: options.targetVersion,
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
await writeMigrationState(effectiveTargetPath, pauseState);
|
|
426
|
+
// Durable backup persists; processes-snapshot persists; we do NOT call
|
|
427
|
+
// deleteProcessesSnapshot or restoreFromDurableBackup here. The agent
|
|
428
|
+
// runs the envelope, calls `gdh migration record-envelope-result` (Plan
|
|
429
|
+
// 73-05), then re-runs `gdh self-update` which resumes from step 8 with
|
|
430
|
+
// wasResume === true.
|
|
103
431
|
return {
|
|
104
|
-
state: "
|
|
432
|
+
state: "needs_envelope",
|
|
105
433
|
fromVersion,
|
|
106
|
-
|
|
107
|
-
|
|
434
|
+
attemptedToVersion: options.targetVersion,
|
|
435
|
+
envelopeRef: chainResult.envelopeRef,
|
|
436
|
+
envelope: chainResult.envelope,
|
|
437
|
+
preservationNotes,
|
|
438
|
+
priorlyApplied: chainResult.applied,
|
|
108
439
|
};
|
|
109
440
|
}
|
|
110
|
-
|
|
111
|
-
|
|
441
|
+
if (chainResult.state === "verify_failed" ||
|
|
442
|
+
chainResult.state === "transform_threw") {
|
|
443
|
+
const restore = await restoreFromDurableBackup({
|
|
444
|
+
targetPath: effectiveTargetPath,
|
|
445
|
+
});
|
|
446
|
+
const failureReason = chainResult.state === "verify_failed"
|
|
447
|
+
? ({
|
|
448
|
+
kind: "verify_failed",
|
|
449
|
+
entryId: chainResult.failedEntryId,
|
|
450
|
+
reason: chainResult.reason,
|
|
451
|
+
})
|
|
452
|
+
: ({
|
|
453
|
+
kind: "transform_threw",
|
|
454
|
+
entryId: chainResult.failedEntryId,
|
|
455
|
+
error: chainResult.error,
|
|
456
|
+
});
|
|
112
457
|
return {
|
|
113
|
-
state: "
|
|
458
|
+
state: "rolled_back_from_durable_backup",
|
|
459
|
+
// WR-03: see WR-03 comment above — use the resolved fromVersion from
|
|
460
|
+
// line 266 (typed `string` semver), not `fromPin` which falls back to "".
|
|
114
461
|
fromVersion,
|
|
115
462
|
attemptedToVersion: options.targetVersion,
|
|
116
|
-
failureReason
|
|
117
|
-
|
|
463
|
+
failureReason,
|
|
464
|
+
preservationNotes,
|
|
465
|
+
restoredPaths: restore.state === "restored" ? restore.restored : [],
|
|
466
|
+
removedPaths: restore.state === "restored" ? restore.removed : [],
|
|
467
|
+
// WR-06: same as above — surface aggregate restore-failure count.
|
|
468
|
+
restoreFailedCount: restore.state === "restored" ? restore.failedCount : 0,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
// Step 9: Inventory set-diff sweep.
|
|
472
|
+
const oldInventory = await readRenderInventory(effectiveTargetPath);
|
|
473
|
+
const supplementaryDeletions = chainResult.state === "applied"
|
|
474
|
+
? chainResult.applied.flatMap((id) => {
|
|
475
|
+
const entry = MIGRATION_REGISTRY_ENTRIES.find((e) => e.id === id);
|
|
476
|
+
return entry?.supplementaryDeletions ?? [];
|
|
477
|
+
})
|
|
478
|
+
: [];
|
|
479
|
+
await runInventorySweep({
|
|
480
|
+
targetPath: effectiveTargetPath,
|
|
481
|
+
oldInventory,
|
|
482
|
+
supplementaryDeletions,
|
|
483
|
+
});
|
|
484
|
+
// Step 10: Write new render-inventory.json + bootstrap migration.json.
|
|
485
|
+
const renderedRelativePaths = uniquePaths(allPlannedRelativePaths);
|
|
486
|
+
await writeRenderInventory(effectiveTargetPath, {
|
|
487
|
+
inventory_version: 1,
|
|
488
|
+
rendered_at: {
|
|
489
|
+
package: options.targetVersion,
|
|
490
|
+
schema: toPair.schema,
|
|
491
|
+
agentContract: toPair.agentContract,
|
|
492
|
+
},
|
|
493
|
+
paths: renderedRelativePaths,
|
|
494
|
+
});
|
|
495
|
+
const baseMigrationState = {
|
|
496
|
+
migration_version: 1,
|
|
497
|
+
last_applied_at: {
|
|
498
|
+
package: options.targetVersion,
|
|
499
|
+
schema: toPair.schema,
|
|
500
|
+
agentContract: toPair.agentContract,
|
|
501
|
+
},
|
|
502
|
+
deferred_actions: [],
|
|
503
|
+
};
|
|
504
|
+
// Step 11: Restart processes from snapshot.
|
|
505
|
+
//
|
|
506
|
+
// On the fresh-run path, stopResult.updatedSnapshot is the in-memory
|
|
507
|
+
// snapshot whose stop_method-per-row was set by the step-4 stop loop. On
|
|
508
|
+
// resume, stopResult is null — step 4 ran in the original attempt, and the
|
|
509
|
+
// on-disk processes-snapshot.json carries the stop_method state we need.
|
|
510
|
+
// Read it fresh from disk in that case (Phase 73 D-11 LOCKED resume-path
|
|
511
|
+
// refactor — captureResult.snapshot.captured_at is undefined on resume, so
|
|
512
|
+
// any deferred-action id below MUST derive from the resume-read snapshot).
|
|
513
|
+
const restartSnapshot = wasResume
|
|
514
|
+
? await readProcessesSnapshot(effectiveTargetPath)
|
|
515
|
+
: stopResult.updatedSnapshot;
|
|
516
|
+
if (wasResume && restartSnapshot === null) {
|
|
517
|
+
// Defensive degraded-state path: the marker said we were mid-resume but
|
|
518
|
+
// processes-snapshot.json is gone. Cannot restart processes blindly;
|
|
519
|
+
// surface as blocked so the agent re-runs a fresh self-update. Mitigates
|
|
520
|
+
// T-73-04-06.
|
|
521
|
+
return {
|
|
522
|
+
state: "blocked",
|
|
523
|
+
reason: "resume_snapshot_missing: processes-snapshot.json was deleted between pause and resume",
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
const restartResult = await restartFromSnapshot({
|
|
527
|
+
snapshot: restartSnapshot,
|
|
528
|
+
env,
|
|
529
|
+
targetPath: effectiveTargetPath,
|
|
530
|
+
});
|
|
531
|
+
if (restartResult.state === "all_restarted") {
|
|
532
|
+
// WR-02: Receipt-before-cleanup ordering. migration.json is the durable
|
|
533
|
+
// success marker (Plan 72-08 D-10 step 12); processes-snapshot.json is the
|
|
534
|
+
// work artifact. If writeMigrationState throws BETWEEN deleteProcessesSnapshot
|
|
535
|
+
// and the migration-state write, the operator is left with snapshot deleted
|
|
536
|
+
// AND migration.json missing — the very state-corruption scenario the
|
|
537
|
+
// staged-write strategy in this phase is designed to prevent. Land the
|
|
538
|
+
// receipt first, then delete the work artifact.
|
|
539
|
+
//
|
|
540
|
+
// Phase 73 D-11: on resume, the success-write must ALSO clear
|
|
541
|
+
// pending_envelope_resume (chain advanced past the envelope). Preserve
|
|
542
|
+
// any existing `envelopes` slot history (D-08) — that is recorded-result
|
|
543
|
+
// evidence and is independent of the in-flight marker.
|
|
544
|
+
const priorAtSuccess = await readMigrationState(effectiveTargetPath);
|
|
545
|
+
const successState = {
|
|
546
|
+
...baseMigrationState,
|
|
547
|
+
...(priorAtSuccess?.envelopes !== undefined
|
|
548
|
+
? { envelopes: priorAtSuccess.envelopes }
|
|
549
|
+
: {}),
|
|
550
|
+
// pending_envelope_resume omitted → cleared. Mitigates T-73-04-03.
|
|
551
|
+
};
|
|
552
|
+
await writeMigrationState(effectiveTargetPath, successState);
|
|
553
|
+
await deleteProcessesSnapshot(effectiveTargetPath);
|
|
554
|
+
// Step 12: Emit report (success).
|
|
555
|
+
return {
|
|
556
|
+
state: "updated",
|
|
557
|
+
fromVersion,
|
|
558
|
+
toVersion: options.targetVersion,
|
|
559
|
+
actions: rebakeActions,
|
|
118
560
|
};
|
|
119
561
|
}
|
|
562
|
+
// some_failed → D-19: create a process_restart deferred action; snapshot persists.
|
|
563
|
+
// Phase 73: on resume, captureResult is null, so the deferred-action id
|
|
564
|
+
// MUST derive from restartSnapshot.captured_at — the resume-read snapshot
|
|
565
|
+
// we just used to drive restart, which carries the original step-3 capture
|
|
566
|
+
// timestamp. On the fresh-run path the two values are identical.
|
|
567
|
+
const failedRows = restartResult.failed;
|
|
568
|
+
const snapshotCapturedAt = wasResume
|
|
569
|
+
? restartSnapshot.captured_at
|
|
570
|
+
: captureResult.snapshot.captured_at;
|
|
571
|
+
const deferredAction = {
|
|
572
|
+
id: `process_restart_${snapshotCapturedAt}`,
|
|
573
|
+
description: "One or more managed processes failed to restart after self-update; the runtime probe will re-attempt on the next gdh status invocation.",
|
|
574
|
+
verify: {
|
|
575
|
+
kind: "process_restart",
|
|
576
|
+
payload: {
|
|
577
|
+
failed_rows: failedRows.map((row) => ({
|
|
578
|
+
kind: row.kind,
|
|
579
|
+
pid: row.pid,
|
|
580
|
+
cmd: row.cmd,
|
|
581
|
+
})),
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
status: "pending",
|
|
585
|
+
};
|
|
586
|
+
// Phase 73: clear pending_envelope_resume on the some_failed path too —
|
|
587
|
+
// the chain advanced past the envelope; only the restart was partial.
|
|
588
|
+
// Preserve envelopes slot history (D-08).
|
|
589
|
+
const priorAtPartial = await readMigrationState(effectiveTargetPath);
|
|
590
|
+
const updatedMigrationState = {
|
|
591
|
+
...baseMigrationState,
|
|
592
|
+
deferred_actions: [deferredAction],
|
|
593
|
+
...(priorAtPartial?.envelopes !== undefined
|
|
594
|
+
? { envelopes: priorAtPartial.envelopes }
|
|
595
|
+
: {}),
|
|
596
|
+
// pending_envelope_resume omitted → cleared.
|
|
597
|
+
};
|
|
598
|
+
await writeMigrationState(effectiveTargetPath, updatedMigrationState);
|
|
599
|
+
return {
|
|
600
|
+
state: "updated_with_deferred_actions",
|
|
601
|
+
fromVersion,
|
|
602
|
+
toVersion: options.targetVersion,
|
|
603
|
+
actions: rebakeActions,
|
|
604
|
+
migrationChainResult: chainResult,
|
|
605
|
+
deferredActions: [{ id: deferredAction.id, kind: deferredAction.verify.kind }],
|
|
606
|
+
restartFailedRows: failedRows,
|
|
607
|
+
preservationNotes,
|
|
608
|
+
};
|
|
120
609
|
}
|
|
121
610
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
611
|
+
* Compute workspace-relative POSIX paths for every plan action that has a
|
|
612
|
+
* concrete absolutePath. Skips abstract actions (run_command,
|
|
613
|
+
* write_local_hints) whose absolutePath is null.
|
|
614
|
+
*
|
|
615
|
+
* WR-08: When an action's `absolutePath` is non-null but resolves OUTSIDE
|
|
616
|
+
* `effectiveTargetPath`, this is a defensive invariant violation — adapter
|
|
617
|
+
* outputs are GDH-controlled, so a violation indicates a real upstream bug
|
|
618
|
+
* (e.g., wrong targetPath resolution upstream) that would silently leave the
|
|
619
|
+
* surface uncaptured by the durable backup and defeat the rollback contract
|
|
620
|
+
* for that surface. Fail loud rather than silently dropping the action.
|
|
125
621
|
*/
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
originals.set(pinConfigPath, await fs.readFile(pinConfigPath, "utf8"));
|
|
622
|
+
function computePlannedRelativePaths(plannedActions, effectiveTargetPath) {
|
|
623
|
+
const relativePaths = [];
|
|
129
624
|
for (const action of plannedActions) {
|
|
130
625
|
if (action.absolutePath === null)
|
|
131
626
|
continue;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Best-effort restore. Iterates captured originals; restores content or rm's
|
|
143
|
-
* previously-absent files. Collects failure messages but continues (rollback-soldier
|
|
144
|
-
* pattern: partial recovery > aborting mid-rollback).
|
|
145
|
-
*/
|
|
146
|
-
async function rollbackOriginals(originals) {
|
|
147
|
-
const failures = [];
|
|
148
|
-
for (const [absolutePath, content] of originals) {
|
|
149
|
-
try {
|
|
150
|
-
if (content === null) {
|
|
151
|
-
await fs.rm(absolutePath, { force: true });
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
await fs.writeFile(absolutePath, content, "utf8");
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
failures.push(`${absolutePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
627
|
+
const rel = path.relative(effectiveTargetPath, action.absolutePath);
|
|
628
|
+
// WR-08: defensive invariant — adapter outputs are GDH-controlled.
|
|
629
|
+
// Out-of-workspace absolutePath signals an upstream bug; throw rather
|
|
630
|
+
// than silently dropping the action (which would defeat the durable-
|
|
631
|
+
// backup rollback contract for that surface).
|
|
632
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
633
|
+
throw new Error(`assertion: action ${action.agent}/${action.kind}/${action.scope}` +
|
|
634
|
+
` (relativePath=${action.relativePath ?? "<null>"}) has absolutePath` +
|
|
635
|
+
` outside target ${effectiveTargetPath}: ${action.absolutePath}`);
|
|
159
636
|
}
|
|
637
|
+
relativePaths.push(rel.split(path.sep).join("/"));
|
|
160
638
|
}
|
|
161
|
-
return
|
|
639
|
+
return relativePaths;
|
|
640
|
+
}
|
|
641
|
+
function uniquePaths(paths) {
|
|
642
|
+
return [...new Set(paths)].sort();
|
|
162
643
|
}
|
|
163
644
|
function formatError(error) {
|
|
164
645
|
return error instanceof Error ? error.message : String(error);
|