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 +31 -0
- package/dist/{apply-OEKVIYBV.js → apply-JBULQGW5.js} +2 -2
- package/dist/{chunk-4AM3LNUQ.js → chunk-E7Z2YB2C.js} +118 -6
- package/dist/{chunk-JXX6EDIP.js → chunk-SHPNLNBR.js} +550 -309
- package/dist/index.js +967 -239
- package/dist/{transaction-D7VRZZX5.js → transaction-EPP7GHXD.js} +1 -1
- package/openpack.json +1 -1
- package/package.json +1 -1
- package/skills/agentwheel/SKILL.md +14 -1
- package/skills/agentwheel-discovery/SKILL.md +1 -1
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-
|
|
9
|
-
import "./chunk-
|
|
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-
|
|
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,
|