agentwheel 0.20.7 → 0.20.9

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 CHANGED
@@ -396,6 +396,37 @@ current directory, then fallback to the current directory. `--all-detected` is a
396
396
  for applying to every runtime marker found in the current directory or `--target-root`; `--all` remains
397
397
  reserved for configured agents.
398
398
 
399
+ ### Target state identity
400
+
401
+ Graph-backed installs keep two target fingerprints with different purposes:
402
+
403
+ - The full target fingerprint records rendering inputs in the graph lock. Adapter code, adapter
404
+ configuration, and adapter module changes therefore remain visible and can produce a real update.
405
+ - The stable state fingerprint selects the graph lock, install manifest, source lock, and apply
406
+ journal. It is derived from the contribution scope and effective destination, including the
407
+ normalized install root and a structured local or SSH endpoint. Adapter implementation evolution
408
+ that resolves to the same destination reuses the installed state instead of orphaning it.
409
+
410
+ Changing an artifact destination within the same effective install root is handled as a planned
411
+ transition from the old path to the new path. Changing the effective install root, local/SSH kind, or
412
+ SSH endpoint selects a different target state. SSH state identity requires an explicit host; a display
413
+ label is not endpoint identity.
414
+
415
+ State created by an older Agentwheel release may be named with the full target fingerprint. For a
416
+ local target, Agentwheel adopts that state only when one canonical graph lock, its v2 install
417
+ manifest, the contribution owner, target details, and graph-lock digest all agree. A supported
418
+ install or uninstall carries an explicit migration and revalidates the old and new state under the
419
+ shared apply lock before replacing the legacy paths. An explicit `stateKey` remains the manifest and
420
+ source-lock key while its legacy graph-lock path moves. Ownership handoff and retirement require a
421
+ full install to migrate first. Status, dependency inspection, uninstall, and journal commands use
422
+ the same discovery rules.
423
+
424
+ Agentwheel refuses ambiguous candidates, a graph lock without its correlated manifest, or
425
+ disagreeing stable and legacy state, and preserves those files for manual reconciliation. A pending
426
+ apply journal that embeds and exactly correlates the missing manifest and graph state remains
427
+ discoverable for recovery. Legacy SSH state is also preserved and refused because the old records do
428
+ not prove which endpoint owned it.
429
+
399
430
  ## Core Ideas
400
431
 
401
432
  **Three places, one direction:**
@@ -5,8 +5,8 @@ import {
5
5
  commitManifestMetadataJournal,
6
6
  recoverPendingApply,
7
7
  uninstall
8
- } from "./chunk-4AM3LNUQ.js";
9
- import "./chunk-JXX6EDIP.js";
8
+ } from "./chunk-E7Z2YB2C.js";
9
+ import "./chunk-SHPNLNBR.js";
10
10
  import "./chunk-7VPI5J5Y.js";
11
11
  export {
12
12
  applyCombinedInstallPlan,
@@ -24,7 +24,7 @@ import {
24
24
  rollbackCompletedOperations,
25
25
  sourceLockPath,
26
26
  writeApplyJournal
27
- } from "./chunk-JXX6EDIP.js";
27
+ } from "./chunk-SHPNLNBR.js";
28
28
  import {
29
29
  pathExists,
30
30
  writeJsonAtomic
@@ -32,9 +32,10 @@ import {
32
32
 
33
33
  // src/install/apply.ts
34
34
  import { execFile } from "child_process";
35
+ import { createHash as createHash4 } from "crypto";
35
36
  import { mkdtemp as mkdtemp2, readFile as readFile8, rm as rm2, writeFile as writeFile8 } from "fs/promises";
36
37
  import { tmpdir as tmpdir2 } from "os";
37
- import { basename as basename2, dirname as dirname8, join as join3 } from "path";
38
+ import { basename as basename2, dirname as dirname8, join as join3, resolve as resolve3 } from "path";
38
39
  import { promisify } from "util";
39
40
 
40
41
  // src/model/graph-lock.ts
@@ -572,6 +573,9 @@ async function computeInstallManifestInventoryRevision(targetRoot, adapter, tran
572
573
  const inventory = (await listInstallManifests(targetRoot, adapter, transport)).map((item) => [item.fileName, item.manifest.revision]);
573
574
  return createHash2("sha256").update(JSON.stringify(inventory)).digest("hex");
574
575
  }
576
+ function computeSourceLockRevision(lock) {
577
+ return lock ? createHash2("sha256").update(canonicalJson(lock)).digest("hex") : null;
578
+ }
575
579
  async function writeInstallManifest(manifest, transport = localTransport) {
576
580
  const next = withManifestRevision(manifest);
577
581
  await transport.writeJsonAtomic(installManifestPath(next.targetRoot, next.adapter, {
@@ -582,6 +586,11 @@ async function writeInstallManifest(manifest, transport = localTransport) {
582
586
  async function writeSourceLock(targetRoot, adapter, lock, transport = localTransport, scope = {}) {
583
587
  await transport.writeJsonAtomic(sourceLockPath(targetRoot, adapter, scope), lock);
584
588
  }
589
+ async function readSourceLock(targetRoot, adapter, transport = localTransport, scope = {}) {
590
+ const path = sourceLockPath(targetRoot, adapter, scope);
591
+ if (!await transport.pathExists(path)) return void 0;
592
+ return sourceLockSchema.parse(JSON.parse(await transport.readFile(path)));
593
+ }
585
594
  async function removeStateFiles(targetRoot, adapter, transport = localTransport, scope = {}) {
586
595
  await transport.rm(installManifestPath(targetRoot, adapter, scope));
587
596
  await transport.rm(sourceLockPath(targetRoot, adapter, scope));
@@ -1442,6 +1451,8 @@ async function applyPlanTransactionally(plan, options = {}) {
1442
1451
  await assertRuntimeJournalGate(plan.targetRoot, plan.adapter, transport, scope);
1443
1452
  await assertRuntimeStateRevision(plan, transport);
1444
1453
  await assertBaseRevision(plan, transport);
1454
+ await assertTargetStateFilePreconditions(plan, transport, options.graphLock?.path);
1455
+ await assertStateMigrationPreconditions(plan, transport);
1445
1456
  const now = (/* @__PURE__ */ new Date()).toISOString();
1446
1457
  const graphLockDigest = options.graphLockDigest ?? plan.graphLockDigest;
1447
1458
  const mutation = mutationMetadataForApplyJournal();
@@ -1470,9 +1481,11 @@ async function applyPlanTransactionally(plan, options = {}) {
1470
1481
  adapterCode: plan.adapterCode,
1471
1482
  entries: []
1472
1483
  },
1473
- sourceLock: options.sourceLock,
1484
+ sourceLock: options.sourceLock ?? plan.stateMigration?.sourceLock ?? void 0,
1474
1485
  graphLockPath: options.graphLock?.path,
1475
- graphLock: options.graphLock?.lock
1486
+ graphLock: options.graphLock?.lock,
1487
+ graphLockRemovePath: plan.stateMigration && plan.stateMigration.fromGraphLockPath !== options.graphLock?.path ? plan.stateMigration.fromGraphLockPath : void 0,
1488
+ stateRemoveKey: plan.stateMigration?.fromStateKey !== plan.stateKey ? plan.stateMigration?.fromStateKey : void 0
1476
1489
  };
1477
1490
  await writeApplyJournal(journal, transport);
1478
1491
  const entries = [];
@@ -1549,6 +1562,8 @@ async function uninstall(plan, options = {}) {
1549
1562
  await assertRuntimeJournalGate(plan.targetRoot, plan.adapter, transport, scope);
1550
1563
  await assertRuntimeStateRevision(plan, transport);
1551
1564
  await assertBaseRevision(plan, transport);
1565
+ await assertTargetStateFilePreconditions(plan, transport, resolvedOptions.graphLock?.path);
1566
+ await assertStateMigrationPreconditions(plan, transport);
1552
1567
  await assertExactMergeRemovalPreconditions(removable, transport);
1553
1568
  const revalidatedSkips = /* @__PURE__ */ new Map();
1554
1569
  for (const operation of skipped) {
@@ -1581,9 +1596,11 @@ async function uninstall(plan, options = {}) {
1581
1596
  operations: resolvedOptions.keepFiles ? [] : removable,
1582
1597
  completed: [],
1583
1598
  manifest: finalManifest,
1599
+ sourceLock: plan.stateMigration?.sourceLock ?? void 0,
1584
1600
  graphLockPath: resolvedOptions.graphLock?.path,
1585
1601
  graphLock: resolvedOptions.graphLock?.lock,
1586
- graphLockRemovePath: resolvedOptions.removeGraphLockPath,
1602
+ graphLockRemovePath: resolvedOptions.removeGraphLockPath ?? (plan.stateMigration?.fromGraphLockPath !== resolvedOptions.graphLock?.path ? plan.stateMigration?.fromGraphLockPath : void 0),
1603
+ stateRemoveKey: plan.stateMigration?.fromStateKey !== plan.stateKey ? plan.stateMigration?.fromStateKey : void 0,
1587
1604
  workspaceConfigPath: resolvedOptions.workspaceConfig?.path,
1588
1605
  workspaceConfig: resolvedOptions.workspaceConfig?.data
1589
1606
  };
@@ -1633,6 +1650,12 @@ async function commitJournalState(journal, transport, entries, now) {
1633
1650
  await writeInstallManifest(manifest, transport);
1634
1651
  }
1635
1652
  if (journal.graphLockPath && journal.graphLock) await writeGraphLock(journal.graphLockPath, journal.graphLock);
1653
+ if (journal.stateRemoveKey && journal.stateRemoveKey !== journal.stateKey) {
1654
+ await removeStateFiles(journal.targetRoot, journal.adapter, transport, {
1655
+ installationType: journal.installationType,
1656
+ stateKey: journal.stateRemoveKey
1657
+ });
1658
+ }
1636
1659
  if (journal.graphLockRemovePath) {
1637
1660
  declareMutationPath(journal.graphLockRemovePath);
1638
1661
  await rm2(journal.graphLockRemovePath, { force: true });
@@ -1680,6 +1703,15 @@ async function assertCommittedJournalState(journal, expectedManifest, transport)
1680
1703
  if (journal.graphLockRemovePath && await pathExists(journal.graphLockRemovePath)) {
1681
1704
  throw new Error(`Graph-lock removal postcheck failed for ${journal.graphLockRemovePath}.`);
1682
1705
  }
1706
+ if (journal.stateRemoveKey && journal.stateRemoveKey !== journal.stateKey) {
1707
+ const removedScope = { installationType: journal.installationType, stateKey: journal.stateRemoveKey };
1708
+ if (await readInstallManifest(journal.targetRoot, journal.adapter, transport, removedScope)) {
1709
+ throw new Error(`Legacy manifest removal postcheck failed for state ${journal.stateRemoveKey}.`);
1710
+ }
1711
+ if (await readSourceLock(journal.targetRoot, journal.adapter, transport, removedScope)) {
1712
+ throw new Error(`Legacy source-lock removal postcheck failed for state ${journal.stateRemoveKey}.`);
1713
+ }
1714
+ }
1683
1715
  if (journal.workspaceConfigPath && journal.workspaceConfig) {
1684
1716
  const actual = JSON.parse(await readFile8(journal.workspaceConfigPath, "utf8"));
1685
1717
  if (canonicalJson2(actual) !== canonicalJson2(journal.workspaceConfig)) {
@@ -1811,6 +1843,16 @@ async function applyOperation(operation, context) {
1811
1843
  if (!operation.manifestHash || !operation.desiredHash) {
1812
1844
  throw new Error(`Invalid keep operation missing manifest/source hash: ${operation.relativeDestPath}`);
1813
1845
  }
1846
+ if (operation.preserveInManifest === false) {
1847
+ if (!await transport.pathExists(operation.destPath)) {
1848
+ throw new Error(`Foreign keep destination disappeared after planning: ${operation.relativeDestPath}`);
1849
+ }
1850
+ const currentHash = await transport.hashPath(operation.destPath);
1851
+ if (currentHash !== operation.currentHash || currentHash !== operation.manifestHash || currentHash !== operation.desiredHash) {
1852
+ throw new Error(`Foreign keep destination changed after planning: ${operation.relativeDestPath}`);
1853
+ }
1854
+ return void 0;
1855
+ }
1814
1856
  return manifestEntryForOperation(operation, {
1815
1857
  now,
1816
1858
  hash: operation.manifestHash,
@@ -2117,13 +2159,80 @@ function manifestEntryForOperation(operation, values) {
2117
2159
  async function assertBaseRevision(plan, transport) {
2118
2160
  const current = await readInstallManifest(plan.targetRoot, plan.adapter, transport, {
2119
2161
  installationType: plan.installationType,
2120
- stateKey: plan.stateKey
2162
+ stateKey: plan.stateMigration?.fromStateKey ?? plan.stateKey
2121
2163
  });
2122
2164
  const currentRevision = current?.revision ?? null;
2123
2165
  if (currentRevision !== plan.baseRevision) {
2124
2166
  throw new Error(`Install manifest changed since planning for ${plan.adapter}; replan needed`);
2125
2167
  }
2126
2168
  }
2169
+ async function assertTargetStateFilePreconditions(plan, transport, graphLockWritePath) {
2170
+ const expected = plan.targetStateFilePreconditions;
2171
+ if (!expected) return;
2172
+ if (graphLockWritePath && resolve3(graphLockWritePath) !== resolve3(expected.graphLockPath)) {
2173
+ throw new Error(
2174
+ `Graph-lock destination changed after planning: expected ${expected.graphLockPath}, found ${graphLockWritePath}; replan needed.`
2175
+ );
2176
+ }
2177
+ let graphLockRevision = null;
2178
+ if (await pathExists(expected.graphLockPath)) {
2179
+ try {
2180
+ const graphLock = await readGraphLock(expected.graphLockPath);
2181
+ graphLockRevision = createHash4("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
2182
+ } catch (error) {
2183
+ throw new Error(
2184
+ `Destination graph lock changed after planning at ${expected.graphLockPath}; replan needed: ` + (error instanceof Error ? error.message : String(error))
2185
+ );
2186
+ }
2187
+ }
2188
+ if (graphLockRevision !== expected.graphLockRevision) {
2189
+ throw new Error(
2190
+ `Destination graph lock changed after planning: expected ${expected.graphLockRevision ?? "missing"}, found ${graphLockRevision ?? "missing"}; replan needed.`
2191
+ );
2192
+ }
2193
+ let sourceLockRevision;
2194
+ try {
2195
+ sourceLockRevision = computeSourceLockRevision(await readSourceLock(plan.targetRoot, plan.adapter, transport, {
2196
+ installationType: plan.installationType,
2197
+ stateKey: plan.stateKey
2198
+ }));
2199
+ } catch (error) {
2200
+ throw new Error(
2201
+ `Destination source lock changed after planning for ${plan.adapter}; replan needed: ` + (error instanceof Error ? error.message : String(error))
2202
+ );
2203
+ }
2204
+ if (sourceLockRevision !== expected.sourceLockRevision) {
2205
+ throw new Error(
2206
+ `Destination source lock changed after planning: expected ${expected.sourceLockRevision ?? "missing"}, found ${sourceLockRevision ?? "missing"}; replan needed.`
2207
+ );
2208
+ }
2209
+ }
2210
+ async function assertStateMigrationPreconditions(plan, transport) {
2211
+ const migration = plan.stateMigration;
2212
+ if (!migration) return;
2213
+ let graphLock;
2214
+ try {
2215
+ graphLock = await readGraphLock(migration.fromGraphLockPath);
2216
+ } catch (error) {
2217
+ throw new Error(
2218
+ `Legacy graph lock changed since planning at ${migration.fromGraphLockPath}; replan needed: ` + (error instanceof Error ? error.message : String(error))
2219
+ );
2220
+ }
2221
+ const graphDigest = createHash4("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
2222
+ if (graphDigest !== migration.fromGraphLockDigest) {
2223
+ throw new Error(
2224
+ `Legacy graph lock changed since planning: expected ${migration.fromGraphLockDigest}, found ${graphDigest}; replan needed.`
2225
+ );
2226
+ }
2227
+ if (!migration.fromStateKey) return;
2228
+ const currentSourceLock = await readSourceLock(plan.targetRoot, plan.adapter, transport, {
2229
+ installationType: plan.installationType,
2230
+ stateKey: migration.fromStateKey
2231
+ });
2232
+ if (canonicalJson2(currentSourceLock ?? null) !== canonicalJson2(migration.sourceLock ?? null)) {
2233
+ throw new Error(`Legacy source lock changed since planning for ${plan.adapter}; replan needed.`);
2234
+ }
2235
+ }
2127
2236
  async function assertExactMergeRemovalPreconditions(operations, transport) {
2128
2237
  for (const operation of operations) {
2129
2238
  if (!operation.exactMergeRemoval) continue;
@@ -2283,11 +2392,14 @@ export {
2283
2392
  canonicalizeGraphLock,
2284
2393
  computeTargetFingerprint,
2285
2394
  legacyUnownedWorkspaceOwner,
2395
+ installManifestV2Schema,
2286
2396
  installManifestSchema,
2287
2397
  readInstallManifest,
2288
2398
  listInstallManifests,
2289
2399
  computeInstallManifestInventoryRevision,
2400
+ computeSourceLockRevision,
2290
2401
  writeInstallManifest,
2402
+ readSourceLock,
2291
2403
  normalizeTargetRoot,
2292
2404
  withManifestRevision,
2293
2405
  computeManifestRevision,