token-harness 0.1.1 → 0.1.2

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/token-harness.mjs CHANGED
@@ -111,6 +111,113 @@ function classifyVersion(observed, range) {
111
111
  return compareVersions(version2, maximum) > 0 ? "unknown-newer" : "in-range";
112
112
  }
113
113
 
114
+ // packages/core/dist/src/domain/compatibility-rows.js
115
+ var COMPATIBILITY_ROWS = [
116
+ {
117
+ harness: "claude",
118
+ harnessVersion: { minimum: "2.1.220", maximum: "2.1.220" },
119
+ provider: "rtk",
120
+ providerVersion: "0.44.0",
121
+ platform: { os: "windows", wsl: false, supported: true, limitation: null },
122
+ configSchema: "claude-settings-json-hooks",
123
+ fixture: "tests/fixtures/rows/rtk-claude-windows",
124
+ verificationTier: "canary"
125
+ },
126
+ {
127
+ harness: "claude",
128
+ harnessVersion: { minimum: "2.1.220", maximum: "2.1.220" },
129
+ provider: "harnesstrim",
130
+ providerVersion: "0.1.0",
131
+ platform: { os: "windows", wsl: false, supported: true, limitation: null },
132
+ // The install writes skill files, not a document this build parses, so the schema it depends on
133
+ // is the skills directory layout rather than `settings.json`.
134
+ configSchema: "claude-skills-directory",
135
+ fixture: "tests/fixtures/rows/harnesstrim-claude-windows",
136
+ verificationTier: "config-only"
137
+ }
138
+ ];
139
+ function classifyRowHarnessVersion(rows, observed) {
140
+ if (rows.length === 0)
141
+ return null;
142
+ const parsed = parseSemanticVersion(observed);
143
+ if (parsed === null)
144
+ return null;
145
+ let lowestMinimum = null;
146
+ let highestMaximum = null;
147
+ for (const row2 of rows) {
148
+ const minimum = parseSemanticVersion(row2.harnessVersion.minimum);
149
+ const maximum = parseSemanticVersion(row2.harnessVersion.maximum);
150
+ if (minimum === null || maximum === null)
151
+ continue;
152
+ if (compareVersions(parsed, minimum) >= 0 && compareVersions(parsed, maximum) <= 0)
153
+ return "in-row";
154
+ if (lowestMinimum === null || compareVersions(minimum, lowestMinimum) < 0)
155
+ lowestMinimum = minimum;
156
+ if (highestMaximum === null || compareVersions(maximum, highestMaximum) > 0)
157
+ highestMaximum = maximum;
158
+ }
159
+ if (lowestMinimum === null || highestMaximum === null)
160
+ return null;
161
+ if (compareVersions(parsed, lowestMinimum) < 0)
162
+ return "unknown-older";
163
+ if (compareVersions(parsed, highestMaximum) > 0)
164
+ return "unknown-newer";
165
+ return "below-range";
166
+ }
167
+ function rowsFor(rows, combination) {
168
+ return rows.filter((row2) => row2.provider === combination.provider && row2.harness === combination.harness && row2.platform.os === combination.os && row2.platform.wsl === combination.wsl && row2.platform.supported);
169
+ }
170
+ function admitManagedMutation(rows, combination) {
171
+ const keyed = rowsFor(rows, combination);
172
+ if (combination.providerVersion === null)
173
+ return {
174
+ state: "refused",
175
+ verdict: "no-row",
176
+ missing: "the exact provider version a row would record could not be established"
177
+ };
178
+ if (combination.harnessVersion === null)
179
+ return {
180
+ state: "refused",
181
+ verdict: "no-row",
182
+ missing: "the exact harness version a row would cover could not be established"
183
+ };
184
+ if (keyed.length === 0)
185
+ return {
186
+ state: "refused",
187
+ verdict: "no-row",
188
+ missing: `a reviewed config schema and fixture for ${combination.provider} on ${combination.harness} at ${combination.providerVersion} on ${combination.os}`
189
+ };
190
+ const versioned = keyed.filter((row3) => row3.providerVersion === combination.providerVersion);
191
+ if (versioned.length === 0) {
192
+ const fixture = keyed[0]?.fixture ?? "a provider fixture";
193
+ return {
194
+ state: "refused",
195
+ verdict: "no-row",
196
+ missing: `a provider fixture covering ${combination.provider} at ${combination.providerVersion} (the nearest row's fixture is ${JSON.stringify(fixture)})`
197
+ };
198
+ }
199
+ const parsedHarness = parseSemanticVersion(combination.harnessVersion);
200
+ const row2 = versioned.find((candidate) => {
201
+ const minimum = parseSemanticVersion(candidate.harnessVersion.minimum);
202
+ const maximum = parseSemanticVersion(candidate.harnessVersion.maximum);
203
+ return minimum !== null && maximum !== null && parsedHarness !== null && compareVersions(parsedHarness, minimum) >= 0 && compareVersions(parsedHarness, maximum) <= 0;
204
+ });
205
+ if (row2 !== void 0)
206
+ return { state: "admitted", row: row2 };
207
+ const verdict = parsedHarness === null ? null : classifyRowHarnessVersion(versioned, combination.harnessVersion);
208
+ if (verdict === null || verdict === "in-row")
209
+ return {
210
+ state: "refused",
211
+ verdict: "no-row",
212
+ missing: `a reviewed config schema and fixture for ${combination.provider} on ${combination.harness}`
213
+ };
214
+ return {
215
+ state: "refused",
216
+ verdict,
217
+ missing: verdict === "unknown-newer" ? `a harness schema for ${combination.harness} newer than every row's maximum (the newest row names schema ${JSON.stringify(versioned[0]?.configSchema ?? "none")})` : verdict === "unknown-older" ? `a harness schema for ${combination.harness} older than every row's minimum (the oldest row names schema ${JSON.stringify(versioned[0]?.configSchema ?? "none")})` : `a harness schema covering ${combination.harness} at ${combination.harnessVersion}, between the rows that name schemas ${JSON.stringify(keyed.map((entry) => entry.configSchema))}`
218
+ };
219
+ }
220
+
114
221
  // packages/core/dist/src/domain/compatibility.js
115
222
  function coversHarness(rule, harness) {
116
223
  return rule.harnesses === "*" || rule.harnesses.includes(harness);
@@ -207,6 +314,7 @@ function harnessId(value) {
207
314
  }
208
315
  return value;
209
316
  }
317
+ var UNATTRIBUTED_PROJECT_ID = "p_unattributed";
210
318
 
211
319
  // packages/core/dist/src/domain/manifest.js
212
320
  var MANIFEST_SCHEMA_VERSION = 1;
@@ -588,12 +696,13 @@ function resolveOwnership(input) {
588
696
  continue;
589
697
  }
590
698
  if (!provider.assignable) {
699
+ const observed = input.observedVersions[provider.id] ?? null;
591
700
  exclusions.push({
592
701
  scope,
593
702
  excluded: provider.id,
594
703
  retained: null,
595
704
  reason: [
596
- `${provider.id} implements ${capability} but no installer state produces it in isolation`,
705
+ observed === null ? `${provider.id} implements ${capability} but no installer state this build could observe produces it in isolation` : `${provider.id} ${observed} implements ${capability} but no installer state at that version produces it in isolation`,
597
706
  "RFC 0003: a capability the provider has but cannot be asked for is not an assignable capability",
598
707
  "It is still detected, adopted, reconciled against the owner, and measured"
599
708
  ]
@@ -634,6 +743,7 @@ function resolveOwnership(input) {
634
743
  }
635
744
  }
636
745
  }
746
+ narrowChannelOverlaps({ ownership, exclusions, conflicts, input });
637
747
  const ownerByScope = new Map(ownership.map((entry) => [formatCapabilityScope(entry.scope), entry.owner]));
638
748
  for (const exclusion of exclusions) {
639
749
  if (exclusion.retained !== null)
@@ -644,6 +754,66 @@ function resolveOwnership(input) {
644
754
  }
645
755
  return { ownership, exclusions, conflicts, pipelineId: derivePipelineId(ownership) };
646
756
  }
757
+ function narrowChannelOverlaps(context) {
758
+ const { ownership, exclusions, conflicts, input } = context;
759
+ const channels = /* @__PURE__ */ new Map();
760
+ for (const entry of ownership) {
761
+ if (entry.mode !== "exclusive")
762
+ continue;
763
+ const key = `${entry.scope.harness}/${entry.scope.toolFamily}/${entry.scope.capability}`;
764
+ const group = channels.get(key) ?? [];
765
+ group.push(entry);
766
+ channels.set(key, group);
767
+ }
768
+ const dropped = /* @__PURE__ */ new Set();
769
+ for (const group of channels.values()) {
770
+ const owners = [...new Set(group.map((entry) => entry.owner))];
771
+ if (owners.length < 2)
772
+ continue;
773
+ const first = group[0];
774
+ if (first === void 0)
775
+ continue;
776
+ const { harness, toolFamily, capability } = first.scope;
777
+ const rule = findCompatibilityRule(input.rules, { providers: owners, harness, capability });
778
+ if (rule === null || rule.outcome !== "narrowed" || rule.retains === void 0) {
779
+ conflicts.push({
780
+ code: "exclusive-scope-contested",
781
+ scope: `${harness}/${toolFamily}/${capability}`,
782
+ claimants: [...owners].sort(),
783
+ detail: [
784
+ `${owners.join(" and ")} each own ${capability} on ${harness}/${toolFamily}, at different interception points`,
785
+ "The interception points differ but the payload does not, so the reduction would run twice",
786
+ rule === null ? "No rule names this pair on this channel, and an unreviewed overlap is not permission" : `Rule ${rule.id} does not say which provider keeps the channel`
787
+ ],
788
+ remediation: `Choose one owner for ${harness}/${toolFamily}/${capability} with \`profile: custom\``
789
+ });
790
+ for (const entry of group)
791
+ dropped.add(entry);
792
+ continue;
793
+ }
794
+ const retains = rule.retains;
795
+ for (const entry of group) {
796
+ if (entry.owner === retains)
797
+ continue;
798
+ dropped.add(entry);
799
+ exclusions.push({
800
+ scope: entry.scope,
801
+ excluded: entry.owner,
802
+ retained: retains,
803
+ reason: [
804
+ `rule ${rule.id} keeps ${capability} on ${harness}/${toolFamily} with ${retains}, at every interception point`,
805
+ `${entry.owner} is narrowed to the install its own flags produce rather than excluded whole`,
806
+ rule.rationale
807
+ ]
808
+ });
809
+ }
810
+ }
811
+ if (dropped.size > 0) {
812
+ const kept = ownership.filter((entry) => !dropped.has(entry));
813
+ ownership.length = 0;
814
+ ownership.push(...kept);
815
+ }
816
+ }
647
817
  function resolveContested(context) {
648
818
  const { scope, claims, capability, input, ownership, exclusions, conflicts } = context;
649
819
  const claimants = claims.map((claim) => claim.provider);
@@ -719,6 +889,41 @@ function resolveContested(context) {
719
889
  }
720
890
  return;
721
891
  }
892
+ if (rule.outcome === "narrowed") {
893
+ const retains = rule.retains;
894
+ if (retains === void 0) {
895
+ conflicts.push({
896
+ code: "compatibility-rule-malformed",
897
+ scope: formatCapabilityScope(scope),
898
+ claimants,
899
+ detail: [
900
+ `Rule ${rule.id} narrows a pair without naming which provider keeps the channel`,
901
+ "Choosing one here would make the resolver decide what the rule exists to record"
902
+ ],
903
+ remediation: `Fix rule ${rule.id} so it names the provider that retains the capability`
904
+ });
905
+ return;
906
+ }
907
+ for (const claimant of claimants) {
908
+ if (claimant === retains)
909
+ continue;
910
+ exclusions.push({
911
+ scope,
912
+ excluded: claimant,
913
+ retained: retains,
914
+ reason: [
915
+ `rule ${rule.id} keeps ${capability} on ${scope.harness} with ${retains}`,
916
+ `${claimant} is narrowed to the install its own flags produce rather than excluded whole`,
917
+ rule.rationale
918
+ ]
919
+ });
920
+ }
921
+ const retainedClaim = claims.find((claim) => claim.provider === retains);
922
+ if (retainedClaim !== void 0) {
923
+ ownership.push({ scope, owner: retains, mode: retainedClaim.mode, order: 0 });
924
+ }
925
+ return;
926
+ }
722
927
  const winner = rule.providers.find((provider) => claimants.includes(provider)) ?? claimants[0];
723
928
  for (const claimant of claimants) {
724
929
  if (claimant === winner)
@@ -749,9 +954,10 @@ var RTK_HARNESSTRIM_SHELL_OVERLAP = {
749
954
  // for different reasons. Listing them individually would imply a fourth harness is exempt.
750
955
  harnesses: "*",
751
956
  capabilities: ["shell.output.reduce"],
752
- outcome: "conflict",
753
- testedVersions: { rtk: "0.42.0", harnesstrim: "0.0.5" },
754
- rationale: "HarnessTrim 0.0.5 reduces the same command output RTK already reduced, and no configuration narrows it: its Claude and Codex adapters match Bash only, and its OpenCode plugin reduces every tool result. Reducing an already reduced payload loses signal and counts the saving twice.",
957
+ outcome: "narrowed",
958
+ retains: RTK,
959
+ testedVersions: { rtk: "0.44.0", harnesstrim: "0.1.0" },
960
+ rationale: "RTK keeps shell output reduction: it rewrites the command, so the output arriving at any later point is already reduced, and reducing it again loses signal and counts the saving twice. HarnessTrim 0.1.0 no longer has to be excluded whole for that \u2014 `--no-hook` and `--no-instructions` produce a Claude install that adds skills and reduces nothing, and `--tools` confines its OpenCode reduction to a chosen tool family. So it is narrowed to the form that leaves this channel alone, rather than refused. At 0.0.5 no flag produced any of those states, which is why this rule read `conflict` and said no configuration narrows it.",
755
961
  fixtures: ["docs/rfcs/0003-capabilities-and-conflicts.md#the-table-is-an-intent"]
756
962
  };
757
963
  var COMPATIBILITY_RULES = [RTK_HARNESSTRIM_SHELL_OVERLAP];
@@ -1270,6 +1476,201 @@ async function queryAvailableVersion(input) {
1270
1476
  }
1271
1477
  return { status: "found", version: version2, destination, diagnostics };
1272
1478
  }
1479
+ var INVENTORY_COMMANDS = {
1480
+ /**
1481
+ * `winget list --id <id> --exact` prints the same dashes-anchored table as `show --versions`,
1482
+ * with the installed version as the last token of the row. The header is localized, which is
1483
+ * why the parse is anchored on the separator and the row tail rather than on a column title.
1484
+ *
1485
+ * A missing package makes winget exit non-zero with a localized "no package found" message,
1486
+ * which this build cannot tell from any other failure — so that case is `failed` at the caller,
1487
+ * never `absent`.
1488
+ */
1489
+ winget: {
1490
+ executable: "winget",
1491
+ args: (packageName) => ["list", "--id", packageName, "--exact"],
1492
+ parse: (stdout, _packageName) => {
1493
+ const lines = stdout.split(/\r?\n/);
1494
+ const separator = lines.findIndex((line) => /^-{3,}\s*$/.test(line.trim()));
1495
+ if (separator < 0)
1496
+ return { status: "unknown", version: null };
1497
+ for (const line of lines.slice(separator + 1)) {
1498
+ const row2 = line.trim();
1499
+ if (row2 === "")
1500
+ continue;
1501
+ const candidate = row2.split(/\s+/).at(-1) ?? "";
1502
+ if (parseSemanticVersion(candidate) === null)
1503
+ return { status: "unknown", version: null };
1504
+ return { status: "captured", version: candidate };
1505
+ }
1506
+ return { status: "unknown", version: null };
1507
+ },
1508
+ verified: false
1509
+ },
1510
+ /**
1511
+ * `cargo install --list` prints one `crate v0.1.0:` line per installed crate, so absence is a
1512
+ * positive answer: the crate simply does not appear. This is the one inventory among the six
1513
+ * that can confirm absence.
1514
+ */
1515
+ cargo: {
1516
+ executable: "cargo",
1517
+ args: () => ["install", "--list"],
1518
+ parse: (stdout, packageName) => {
1519
+ const pattern = new RegExp(`^\\s*${packageName}\\s+v(\\S+)\\s*:`, "m");
1520
+ const match = pattern.exec(stdout);
1521
+ if (match === null)
1522
+ return { status: "absent", version: null };
1523
+ const candidate = match[1] ?? "";
1524
+ if (parseSemanticVersion(candidate) === null)
1525
+ return { status: "unknown", version: null };
1526
+ return { status: "captured", version: candidate };
1527
+ },
1528
+ verified: false
1529
+ },
1530
+ npm: {
1531
+ executable: "npm",
1532
+ args: (packageName) => ["ls", "-g", packageName, "--depth=0"],
1533
+ parse: (stdout, packageName) => {
1534
+ const pattern = new RegExp(`(?:^|[^@\\w.-])${packageName}@(\\d[^\\s]*)`);
1535
+ const match = pattern.exec(stdout);
1536
+ const candidate = match?.[1] ?? null;
1537
+ if (candidate === null || parseSemanticVersion(candidate) === null) {
1538
+ return { status: "unknown", version: null };
1539
+ }
1540
+ return { status: "captured", version: candidate };
1541
+ },
1542
+ verified: false
1543
+ },
1544
+ homebrew: {
1545
+ executable: "brew",
1546
+ args: (packageName) => ["list", "--versions", packageName],
1547
+ parse: (stdout, packageName) => {
1548
+ const pattern = new RegExp(`^${packageName}\\s+(.+)$`);
1549
+ const match = pattern.exec(stdout.trim());
1550
+ const candidate = match?.[1]?.trim().split(/\s+/).at(-1) ?? null;
1551
+ if (candidate === null || parseSemanticVersion(candidate) === null) {
1552
+ return { status: "unknown", version: null };
1553
+ }
1554
+ return { status: "captured", version: candidate };
1555
+ },
1556
+ verified: false
1557
+ },
1558
+ uv: {
1559
+ executable: "uv",
1560
+ args: () => ["tool", "list"],
1561
+ parse: (stdout, packageName) => {
1562
+ const pattern = new RegExp(`^${packageName}\\s+v?(\\S+)`, "m");
1563
+ const match = pattern.exec(stdout);
1564
+ if (match === null)
1565
+ return { status: "absent", version: null };
1566
+ const candidate = match[1] ?? "";
1567
+ if (parseSemanticVersion(candidate) === null)
1568
+ return { status: "unknown", version: null };
1569
+ return { status: "captured", version: candidate };
1570
+ },
1571
+ verified: false
1572
+ },
1573
+ pipx: {
1574
+ executable: "pipx",
1575
+ args: () => ["list"],
1576
+ parse: (stdout, packageName) => {
1577
+ const pattern = new RegExp(`^\\s*${packageName}\\s+(\\S+)`, "m");
1578
+ const match = pattern.exec(stdout);
1579
+ if (match === null)
1580
+ return { status: "absent", version: null };
1581
+ const candidate = match[1] ?? "";
1582
+ if (parseSemanticVersion(candidate) === null)
1583
+ return { status: "unknown", version: null };
1584
+ return { status: "captured", version: candidate };
1585
+ },
1586
+ verified: false
1587
+ }
1588
+ };
1589
+ function channelCanReportInventory(channelId) {
1590
+ return Object.hasOwn(INVENTORY_COMMANDS, channelId);
1591
+ }
1592
+ var DEFAULT_INVENTORY_TIMEOUT_MS = 6e4;
1593
+ async function queryPackageInventory(input) {
1594
+ const recipe = INVENTORY_COMMANDS[input.channel];
1595
+ if (recipe === void 0) {
1596
+ return {
1597
+ channel: input.channel,
1598
+ packageName: input.packageName,
1599
+ status: "unsupported",
1600
+ version: null,
1601
+ diagnostics: [
1602
+ diagnostic({
1603
+ severity: "warning",
1604
+ code: "inventory-query-unsupported",
1605
+ message: `This build cannot ask ${input.channel} what it has installed`,
1606
+ remediation: null
1607
+ })
1608
+ ]
1609
+ };
1610
+ }
1611
+ if (input.runner === null) {
1612
+ return {
1613
+ channel: input.channel,
1614
+ packageName: input.packageName,
1615
+ status: "failed",
1616
+ version: null,
1617
+ diagnostics: [
1618
+ diagnostic({
1619
+ severity: "error",
1620
+ code: "no-process-runner",
1621
+ message: "No process runner is available, so no inventory can be captured",
1622
+ remediation: null
1623
+ })
1624
+ ]
1625
+ };
1626
+ }
1627
+ const diagnostics = [];
1628
+ if (!recipe.verified) {
1629
+ diagnostics.push(diagnostic({
1630
+ severity: "warning",
1631
+ code: "inventory-query-unverified",
1632
+ message: `The ${input.channel} inventory query follows that tool's documented output but has not been observed working`,
1633
+ remediation: "Confirm the rollback receipt after any failure"
1634
+ }));
1635
+ }
1636
+ const outcome2 = await input.runner.run({
1637
+ executable: recipe.executable,
1638
+ args: recipe.args(input.packageName),
1639
+ cwd: input.cwd,
1640
+ timeoutMs: input.timeoutMs ?? DEFAULT_INVENTORY_TIMEOUT_MS
1641
+ });
1642
+ if (outcome2.failure !== null || outcome2.exitCode !== 0) {
1643
+ diagnostics.push(diagnostic({
1644
+ severity: "warning",
1645
+ code: "inventory-query-failed",
1646
+ message: outcome2.failure === null ? `${outcome2.displayCommand} exited with ${String(outcome2.exitCode)}` : `${recipe.executable} could not be run: ${outcome2.failure.reason}`,
1647
+ remediation: null
1648
+ }));
1649
+ return {
1650
+ channel: input.channel,
1651
+ packageName: input.packageName,
1652
+ status: "failed",
1653
+ version: null,
1654
+ diagnostics
1655
+ };
1656
+ }
1657
+ const reading = recipe.parse(outcome2.stdout, input.packageName);
1658
+ if (reading.status === "unknown") {
1659
+ diagnostics.push(diagnostic({
1660
+ severity: "warning",
1661
+ code: "inventory-query-unreadable",
1662
+ message: `${recipe.executable} answered, but its output did not name an installed version in the form this build reads`,
1663
+ remediation: null
1664
+ }));
1665
+ }
1666
+ return {
1667
+ channel: input.channel,
1668
+ packageName: input.packageName,
1669
+ status: reading.status,
1670
+ version: reading.version,
1671
+ diagnostics
1672
+ };
1673
+ }
1273
1674
  var DEFAULT_INSTALL_TIMEOUT_MS = 6e5;
1274
1675
  async function runPackageManagerInstall(input) {
1275
1676
  const { action } = input;
@@ -1349,14 +1750,119 @@ async function runPackageManagerInstall(input) {
1349
1750
  }));
1350
1751
  return { status: "failed", diagnostics };
1351
1752
  }
1352
- diagnostics.push(diagnostic({
1353
- severity: "info",
1354
- code: "install-not-reversible",
1355
- message: `${action.packageName} was installed through ${action.packageManager}; a rollback restores files and will not uninstall it`,
1356
- remediation: null
1357
- }));
1358
1753
  return { status: "installed", diagnostics };
1359
1754
  }
1755
+ async function restorePackageInventory(input) {
1756
+ const { capture } = input;
1757
+ if (capture.status === "absent") {
1758
+ return {
1759
+ restored: false,
1760
+ diagnostics: [
1761
+ diagnostic({
1762
+ severity: "info",
1763
+ code: "package-inventory-unrestored",
1764
+ message: `${capture.packageName} was not installed before the transaction; restoring that absence would require an uninstall command, so it stays installed`,
1765
+ remediation: null
1766
+ })
1767
+ ]
1768
+ };
1769
+ }
1770
+ if (capture.status !== "captured" || capture.version === null) {
1771
+ return {
1772
+ restored: false,
1773
+ diagnostics: [
1774
+ diagnostic({
1775
+ severity: "info",
1776
+ code: "package-inventory-unrestored",
1777
+ message: `${capture.packageName} was installed without a captured inventory, so it could not be restored`,
1778
+ remediation: null
1779
+ })
1780
+ ]
1781
+ };
1782
+ }
1783
+ if (input.runner === null) {
1784
+ return {
1785
+ restored: false,
1786
+ diagnostics: [
1787
+ diagnostic({
1788
+ severity: "info",
1789
+ code: "package-inventory-unrestored",
1790
+ message: `${capture.packageName} could not be restored: no process runner is available`,
1791
+ remediation: null
1792
+ })
1793
+ ]
1794
+ };
1795
+ }
1796
+ const restoreAction = {
1797
+ kind: "package-manager-install",
1798
+ id: `restore-${capture.packageName}`,
1799
+ riskClass: "delegated",
1800
+ requiresNetwork: true,
1801
+ requiresElevation: false,
1802
+ affectedPaths: [],
1803
+ affectedProcesses: [capture.channel],
1804
+ preconditions: [],
1805
+ postconditions: [],
1806
+ rollbackData: "none",
1807
+ explanation: `Restore ${capture.packageName} to ${capture.version} after a rollback`,
1808
+ packageManager: capture.channel,
1809
+ packageName: capture.packageName,
1810
+ version: capture.version
1811
+ };
1812
+ const result = await runPackageManagerInstall({
1813
+ action: restoreAction,
1814
+ runner: input.runner,
1815
+ cwd: input.cwd,
1816
+ ...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs }
1817
+ });
1818
+ if (result.status !== "installed") {
1819
+ return {
1820
+ restored: false,
1821
+ diagnostics: [
1822
+ ...result.diagnostics,
1823
+ diagnostic({
1824
+ severity: "error",
1825
+ code: "package-restore-failed",
1826
+ message: `${capture.packageName} could not be restored to ${capture.version} through ${capture.channel}`,
1827
+ path: null,
1828
+ remediation: `Install ${capture.packageName} at ${capture.version} yourself, then run the command again`
1829
+ })
1830
+ ]
1831
+ };
1832
+ }
1833
+ const verified = await queryPackageInventory({
1834
+ channel: capture.channel,
1835
+ packageName: capture.packageName,
1836
+ runner: input.runner,
1837
+ cwd: input.cwd,
1838
+ ...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs }
1839
+ });
1840
+ if (verified.status !== "captured" || verified.version !== capture.version) {
1841
+ return {
1842
+ restored: false,
1843
+ diagnostics: [
1844
+ ...verified.diagnostics,
1845
+ diagnostic({
1846
+ severity: "error",
1847
+ code: "package-restore-failed",
1848
+ message: `${capture.packageName} was reinstalled but the inventory does not report ${capture.version}`,
1849
+ remediation: `Confirm what ${capture.channel} has installed and correct it by hand`
1850
+ })
1851
+ ]
1852
+ };
1853
+ }
1854
+ return {
1855
+ restored: true,
1856
+ diagnostics: [
1857
+ diagnostic({
1858
+ severity: "info",
1859
+ code: "package-inventory-restored",
1860
+ message: `${capture.packageName} was restored to ${capture.version} through ${capture.channel} and the inventory was re-read`,
1861
+ remediation: null
1862
+ })
1863
+ ]
1864
+ };
1865
+ }
1360
1866
 
1361
1867
  // packages/core/dist/src/state/json-merge.js
1362
1868
  function isJsonObject(value) {
@@ -1776,7 +2282,8 @@ function outcome(action, status, parts = {}) {
1776
2282
  status,
1777
2283
  snapshots: parts.snapshots ?? [],
1778
2284
  ownership: parts.ownership ?? [],
1779
- diagnostics: parts.diagnostics ?? []
2285
+ diagnostics: parts.diagnostics ?? [],
2286
+ packageInventory: parts.packageInventory ?? null
1780
2287
  };
1781
2288
  }
1782
2289
  function drift(action, path, message) {
@@ -2055,6 +2562,8 @@ async function scanTree(fs, path, entries = []) {
2055
2562
  return entries;
2056
2563
  }
2057
2564
  function equalTreeEntry(left, right) {
2565
+ if (left === void 0 && right === void 0)
2566
+ return true;
2058
2567
  return left !== void 0 && right !== void 0 && left.kind === right.kind && left.digest === right.digest;
2059
2568
  }
2060
2569
  async function applyDelegatedProviderInstall(action, context) {
@@ -2212,13 +2721,38 @@ async function applyAction(action, context) {
2212
2721
  }
2213
2722
  }
2214
2723
  async function applyPackageManagerInstall(action, context) {
2724
+ const wantsInventory = action.rollbackData === "package-inventory" && !action.requiresElevation;
2725
+ const packageInventory = wantsInventory && context.runner !== void 0 ? await queryPackageInventory({
2726
+ channel: action.packageManager,
2727
+ packageName: action.packageName,
2728
+ runner: context.runner,
2729
+ cwd: context.cwd ?? ""
2730
+ }) : null;
2215
2731
  const result = await runPackageManagerInstall({
2216
2732
  action,
2217
2733
  runner: context.runner ?? null,
2218
2734
  cwd: context.cwd ?? ""
2219
2735
  });
2220
2736
  const status = result.status === "installed" ? "applied" : result.status === "refused" ? "refused" : "failed";
2221
- return outcome(action, status, { diagnostics: result.diagnostics });
2737
+ const diagnostics = [...result.diagnostics];
2738
+ if (status === "applied") {
2739
+ if (packageInventory?.status === "captured" && packageInventory.version !== null) {
2740
+ diagnostics.push(diagnostic({
2741
+ severity: "info",
2742
+ code: "install-inventory-captured",
2743
+ message: `${action.packageName} was installed through ${action.packageManager}; the prior state (${packageInventory.version}) was captured, so a rollback can restore it`,
2744
+ remediation: null
2745
+ }));
2746
+ } else {
2747
+ diagnostics.push(diagnostic({
2748
+ severity: "info",
2749
+ code: "install-not-reversible",
2750
+ message: `${action.packageName} was installed through ${action.packageManager}; a rollback restores files and will not uninstall it`,
2751
+ remediation: null
2752
+ }));
2753
+ }
2754
+ }
2755
+ return outcome(action, status, { diagnostics, packageInventory });
2222
2756
  }
2223
2757
 
2224
2758
  // packages/core/dist/src/state/journal.js
@@ -2814,6 +3348,24 @@ async function verifyRestoration(fs, snapshots) {
2814
3348
  }
2815
3349
  return unrestored;
2816
3350
  }
3351
+ async function restorePackageInventories(records, runner, cwd) {
3352
+ const diagnostics = [];
3353
+ for (const record of records) {
3354
+ if (record.kind !== "package-manager-install")
3355
+ continue;
3356
+ if (record.status !== "applied")
3357
+ continue;
3358
+ if (record.packageInventory === null || record.packageInventory === void 0)
3359
+ continue;
3360
+ const outcome2 = await restorePackageInventory({
3361
+ capture: record.packageInventory,
3362
+ runner,
3363
+ cwd
3364
+ });
3365
+ diagnostics.push(...outcome2.diagnostics);
3366
+ }
3367
+ return diagnostics;
3368
+ }
2817
3369
  async function executeTransaction(request) {
2818
3370
  const startedAt = request.now();
2819
3371
  const entries = [];
@@ -2857,7 +3409,10 @@ async function executeTransaction(request) {
2857
3409
  restoreError = error instanceof Error ? error.message : String(error);
2858
3410
  }
2859
3411
  const unrestored = await verifyRestoration(request.fs, snapshots);
2860
- if (restoreError === null && unrestored.length === 0) {
3412
+ const packageDiagnostics = await restorePackageInventories(applied, request.runner ?? null, request.projectRoot);
3413
+ diagnostics.push(...packageDiagnostics);
3414
+ const packageRestoreFailed = packageDiagnostics.some((entry) => entry.severity === "error");
3415
+ if (restoreError === null && unrestored.length === 0 && !packageRestoreFailed) {
2861
3416
  diagnostics.push(diagnostic({
2862
3417
  severity: "info",
2863
3418
  code: "transaction-rolled-back",
@@ -2885,6 +3440,7 @@ async function executeTransaction(request) {
2885
3440
  status: "failed",
2886
3441
  snapshots: [],
2887
3442
  ownership: [],
3443
+ packageInventory: null,
2888
3444
  diagnostics: [
2889
3445
  diagnostic({
2890
3446
  severity: "error",
@@ -2902,7 +3458,8 @@ async function executeTransaction(request) {
2902
3458
  status: result.status,
2903
3459
  snapshots: result.snapshots,
2904
3460
  ownership: result.ownership,
2905
- diagnostics: result.diagnostics
3461
+ diagnostics: result.diagnostics,
3462
+ packageInventory: result.packageInventory
2906
3463
  };
2907
3464
  entries.push(entry);
2908
3465
  applied.push(result);
@@ -2987,7 +3544,10 @@ async function rollbackTransaction(request) {
2987
3544
  restoreError = error instanceof Error ? error.message : String(error);
2988
3545
  }
2989
3546
  const unrestored = await verifyRestoration(request.fs, snapshots);
2990
- if (restoreError === null && unrestored.length === 0) {
3547
+ const packageDiagnostics = await restorePackageInventories(journal.entries, request.runner ?? null, request.cwd ?? journal.projectRoot);
3548
+ diagnostics.push(...packageDiagnostics);
3549
+ const packageRestoreFailed = packageDiagnostics.some((entry) => entry.severity === "error");
3550
+ if (restoreError === null && unrestored.length === 0 && !packageRestoreFailed) {
2991
3551
  journal.outcome = "rolled-back";
2992
3552
  journal.ownership = [];
2993
3553
  journal.finishedAt = request.now();
@@ -5004,20 +5564,47 @@ var MANIFEST3 = {
5004
5564
  * configuration must be able to exit 0. A declared limitation is not a problem, and reporting
5005
5565
  * it as one is the fastest way to teach users to ignore the exit code."
5006
5566
  *
5007
- * A single point rather than a range, matching the Codex adapter: one version was observed, so
5008
- * one version is claimed. An older 1.x reports `unknown-older`, which is a warning and not a
5567
+ * A range now, because spike 9.1 observed two more: the CLI at `1.18.11` and OpenCode Desktop at
5568
+ * `1.18.14`, both loading plugins from the same directories. The span is what was watched, not a
5569
+ * guess that every 1.18.x behaves alike — and the two ends differ in a way that matters, which is
5570
+ * exactly why both are named. An older 1.x reports `unknown-older`, which is a warning and not a
5009
5571
  * problem, and a newer one reports `unknown-newer` — which is then a true statement.
5010
5572
  */
5011
- testedVersions: { minimum: "1.18.9", maximum: "1.18.9" },
5573
+ testedVersions: { minimum: "1.18.9", maximum: "1.18.14" },
5012
5574
  // RFC 0007: a managed provider can emit a receipt, but an adopted generated wrapper
5013
5575
  // has no externally observable receipt. The harness adapter alone therefore declares
5014
5576
  // the achievable baseline, not a promise about a provider it does not own.
5015
5577
  verificationTier: "config-only",
5016
5578
  versionCommand: { executable: "opencode", args: ["--version"] },
5017
- interceptionPoints: [{ scopeId: "tool-execute-after", eventName: "tool.execute.after" }],
5579
+ interceptionPoints: [
5580
+ { scopeId: "tool-execute-before", eventName: "tool.execute.before" },
5581
+ { scopeId: "tool-execute-after", eventName: "tool.execute.after" }
5582
+ ],
5583
+ /**
5584
+ * Two kinds of entry, because OpenCode has two ways to register a plugin and only one of them is
5585
+ * a document.
5586
+ *
5587
+ * The `opencode.jsonc` files carry a `plugin` string array. The directories are auto-loaded: any
5588
+ * module dropped in one is registered without being named anywhere, which is how both real
5589
+ * integrations install — `rtk init -g --opencode` writes `.config/opencode/plugins/rtk.ts` and
5590
+ * touches no JSON at all, and HarnessTrim's generated wrapper says in its own header not to
5591
+ * reference it from `opencode.json`. An adapter that read only the array would report every such
5592
+ * installation as absent, which is the state this file was in before spike 9.1.
5593
+ *
5594
+ * Both spellings are listed because both load. Spike 9.1 put a probe in each and one run fired
5595
+ * both; the upstream documentation names the plural, the binary carries both strings, and the two
5596
+ * shipped integrations disagree with each other about which to use.
5597
+ *
5598
+ * `markers` is the parser because there is nothing to parse: the presence of a file is the
5599
+ * configuration, and its contents are a plugin module rather than a document this build reads.
5600
+ */
5018
5601
  configFiles: [
5019
5602
  { path: ".config/opencode/opencode.jsonc", scope: "user", parser: "jsonc", primary: true },
5020
- { path: "opencode.jsonc", scope: "project", parser: "jsonc", primary: false }
5603
+ { path: "opencode.jsonc", scope: "project", parser: "jsonc", primary: false },
5604
+ { path: ".config/opencode/plugins", scope: "user", parser: "markers", primary: false },
5605
+ { path: ".config/opencode/plugin", scope: "user", parser: "markers", primary: false },
5606
+ { path: ".opencode/plugins", scope: "project", parser: "markers", primary: false },
5607
+ { path: ".opencode/plugin", scope: "project", parser: "markers", primary: false }
5021
5608
  ],
5022
5609
  toolFamilies: [
5023
5610
  { id: "tool.execute", platforms: ["windows", "macos", "linux"], executesShellCommands: true }
@@ -5034,7 +5621,38 @@ function pluginEntries(document2) {
5034
5621
  return [];
5035
5622
  return document2["plugin"].filter((item) => typeof item === "string");
5036
5623
  }
5624
+ var PLUGIN_MODULE_PATTERN = /\.(ts|js|mjs|cjs|mts|cts)$/i;
5625
+ var PLUGIN_POINTS = ["tool-execute-before", "tool-execute-after"];
5626
+ async function resolvePluginDirectory(declaration, context) {
5627
+ const path = resolveConfigPath(declaration, context);
5628
+ const stat3 = await context.fs.stat(path);
5629
+ if (stat3 === null || stat3.kind !== "directory") {
5630
+ return {
5631
+ declaration,
5632
+ path,
5633
+ exists: false,
5634
+ parsed: false,
5635
+ configuredPoints: [],
5636
+ matchers: [],
5637
+ commands: []
5638
+ };
5639
+ }
5640
+ const modules = (await context.fs.readDirectory(path)).filter((entry) => PLUGIN_MODULE_PATTERN.test(entry));
5641
+ return {
5642
+ declaration,
5643
+ path,
5644
+ exists: true,
5645
+ // A directory listing cannot fail to parse. `markers` declares that there is nothing to read.
5646
+ parsed: true,
5647
+ configuredPoints: modules.length > 0 ? [...PLUGIN_POINTS] : [],
5648
+ // The plugin API sees every tool result; this is not a shell matcher.
5649
+ matchers: modules.length > 0 ? ["tool.execute"] : [],
5650
+ commands: modules.map((entry) => context.fs.join(path, entry))
5651
+ };
5652
+ }
5037
5653
  async function resolveConfig3(declaration, context) {
5654
+ if (declaration.parser === "markers")
5655
+ return resolvePluginDirectory(declaration, context);
5038
5656
  const path = resolveConfigPath(declaration, context);
5039
5657
  const stat3 = await context.fs.stat(path);
5040
5658
  if (stat3 === null || stat3.kind !== "file") {
@@ -5066,7 +5684,9 @@ async function resolveConfig3(declaration, context) {
5066
5684
  path,
5067
5685
  exists: true,
5068
5686
  parsed: true,
5069
- configuredPoints: plugins.length > 0 ? ["tool-execute-after"] : [],
5687
+ // A `plugin` array entry is the same kind of thing as a module in a plugin directory: a
5688
+ // registration whose hooks are only known once it runs. Same claim, same reason.
5689
+ configuredPoints: plugins.length > 0 ? [...PLUGIN_POINTS] : [],
5070
5690
  // The plugin API sees every tool result; this is not a shell matcher.
5071
5691
  matchers: plugins.length > 0 ? ["tool.execute"] : [],
5072
5692
  commands: plugins
@@ -5234,7 +5854,14 @@ var CLAUDE_ARTIFACT_DIGESTS = {
5234
5854
  "scaffold-fast/SKILL.md": "sha256:1a18d52c4d335fbd3f74e2559bc20d6346193a40eef7b85149f51f44f697d182"
5235
5855
  };
5236
5856
  var OPENCODE2 = harnessId("opencode");
5857
+ var MODE_CARRYING_HARNESSES = /* @__PURE__ */ new Set([
5858
+ OPENCODE2,
5859
+ harnessId("hermes"),
5860
+ harnessId("pi"),
5861
+ harnessId("omp")
5862
+ ]);
5237
5863
  var HOOK_COMMAND_PATTERN = /(^|[\\/\s"'])harnesstrim(\.cmd|\.exe)?([\s"']|$)/i;
5864
+ var PLUGIN_MODULE_PATTERN2 = /(^|[\\/])harnesstrim\.(ts|js|mjs|cjs|mts|cts)$/i;
5238
5865
  var MANIFEST4 = {
5239
5866
  schemaVersion: MANIFEST_SCHEMA_VERSION,
5240
5867
  id: HARNESSTRIM2,
@@ -5311,12 +5938,17 @@ var MANIFEST4 = {
5311
5938
  }
5312
5939
  ],
5313
5940
  metrics: {
5314
- // RFC 0005 §Importer degradation policy: `legacy` is the character-only `TrimEvent` JSONL, and
5315
- // it is "a supported steady state, not a warning". `native` would need upstream to expose token
5316
- // counts and native ids, which `0.0.5` does not.
5941
+ // RFC 0005 §Importer degradation policy. `native` since PLAN §15 item 43d: the `0.1.0`
5942
+ // telemetry lines carry a producer `eventId` and nullable token counts, so dedup uses the
5943
+ // native ID and a token count is a token count where one exists. Legacy schema 0 lines
5944
+ // still parse, stay `estimated-local`, and never merge with the native figures.
5317
5945
  source: "jsonl",
5318
- mode: "legacy",
5319
- locations: [".harnesstrim/metrics.jsonl", "~/.hermes/harnesstrim-metrics.jsonl"]
5946
+ mode: "native",
5947
+ // PLAN §15 item 25: Hermes has no adapter, tested range or tier, so the home-relative path that
5948
+ // names it is not read. Item 30 readmits it together with the adapter and a matrix row; the
5949
+ // registry assertion in `registries.test.ts` refuses a home-relative location that names a
5950
+ // harness the registry does not know.
5951
+ locations: [".harnesstrim/metrics.jsonl"]
5320
5952
  },
5321
5953
  delegatedInstallReview: {
5322
5954
  upstreamVersion: "0.0.7",
@@ -5330,7 +5962,195 @@ var MANIFEST4 = {
5330
5962
  var TESTED_UPSTREAM = "0.0.7";
5331
5963
  var TESTED_VERSIONS = { minimum: "0.0.5", maximum: TESTED_UPSTREAM };
5332
5964
  function identifiesCommand(command) {
5333
- return HOOK_COMMAND_PATTERN.test(command);
5965
+ return HOOK_COMMAND_PATTERN.test(command) || PLUGIN_MODULE_PATTERN2.test(command);
5966
+ }
5967
+ function isStringArray(value) {
5968
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
5969
+ }
5970
+ function parseHarnessCapabilities(value) {
5971
+ if (typeof value !== "object" || value === null)
5972
+ return null;
5973
+ const record = value;
5974
+ const surfaces = record["surfaces"];
5975
+ const writeSet = record["writeSet"];
5976
+ const narrowing = record["narrowing"];
5977
+ if (!isStringArray(surfaces) || !isStringArray(writeSet) || !Array.isArray(narrowing)) {
5978
+ return null;
5979
+ }
5980
+ const flags = [];
5981
+ for (const item of narrowing) {
5982
+ if (typeof item !== "object" || item === null)
5983
+ return null;
5984
+ const flag = item["flag"];
5985
+ const produces = item["produces"];
5986
+ if (typeof flag !== "string" || typeof produces !== "string")
5987
+ return null;
5988
+ flags.push({ flag, produces });
5989
+ }
5990
+ return {
5991
+ adapter: typeof record["adapter"] === "string" ? record["adapter"] : "",
5992
+ surfaces,
5993
+ writeSet,
5994
+ narrowing: flags
5995
+ };
5996
+ }
5997
+ function parseCapabilities(stdout) {
5998
+ let parsed;
5999
+ try {
6000
+ parsed = JSON.parse(stdout);
6001
+ } catch {
6002
+ return null;
6003
+ }
6004
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
6005
+ return null;
6006
+ const record = parsed;
6007
+ const version2 = record["version"];
6008
+ const harnesses = record["harnesses"];
6009
+ if (typeof version2 !== "string" || typeof harnesses !== "object" || harnesses === null) {
6010
+ return null;
6011
+ }
6012
+ const entries = {};
6013
+ for (const [harness, value] of Object.entries(harnesses)) {
6014
+ const entry = parseHarnessCapabilities(value);
6015
+ if (entry === null)
6016
+ return null;
6017
+ entries[harness] = entry;
6018
+ }
6019
+ return { version: version2, harnesses: entries };
6020
+ }
6021
+ async function probeCapabilities(context) {
6022
+ const outcome2 = await context.runner.run({
6023
+ executable: "harnesstrim",
6024
+ args: ["capabilities"],
6025
+ cwd: context.projectRoot,
6026
+ timeoutMs: 2e4
6027
+ });
6028
+ if (outcome2.failure !== null || outcome2.exitCode !== 0) {
6029
+ return {
6030
+ capabilities: null,
6031
+ evidence: [
6032
+ evidence({
6033
+ kind: "absence",
6034
+ source: "harnesstrim capabilities",
6035
+ detail: "did not answer with a machine-readable declaration"
6036
+ })
6037
+ ]
6038
+ };
6039
+ }
6040
+ const capabilities = parseCapabilities(outcome2.stdout);
6041
+ if (capabilities === null) {
6042
+ return {
6043
+ capabilities: null,
6044
+ evidence: [
6045
+ evidence({
6046
+ kind: "version-output",
6047
+ source: "harnesstrim capabilities",
6048
+ path: outcome2.executablePath,
6049
+ detail: "answered, but not with a declaration this build can read"
6050
+ })
6051
+ ]
6052
+ };
6053
+ }
6054
+ return {
6055
+ capabilities,
6056
+ evidence: [
6057
+ evidence({
6058
+ kind: "version-output",
6059
+ source: "harnesstrim capabilities",
6060
+ path: outcome2.executablePath,
6061
+ detail: `declared ${capabilities.version} for ${Object.keys(capabilities.harnesses).join(", ")}`
6062
+ })
6063
+ ]
6064
+ };
6065
+ }
6066
+ var SURFACE_ANCHORS = {
6067
+ "post-tool-use": "PostToolUse",
6068
+ "tool-execute-after": "tool.execute.after"
6069
+ };
6070
+ function surfaceMatchesDeclaration(surface, declaration) {
6071
+ const pointAnchor = SURFACE_ANCHORS[declaration.interceptionPoint];
6072
+ if (pointAnchor !== void 0 && !surface.includes(pointAnchor))
6073
+ return false;
6074
+ if (declaration.toolFamily !== EVERY_TOOL_FAMILY && !surface.includes(declaration.toolFamily)) {
6075
+ return false;
6076
+ }
6077
+ return true;
6078
+ }
6079
+ function writeSetPath(entry) {
6080
+ return entry.replace(/\s*\([^)]*\)\s*$/, "").replaceAll("\\", "/");
6081
+ }
6082
+ function coveredBy(path, declared) {
6083
+ const base = declared.endsWith("/") ? declared : `${declared}/`;
6084
+ return path === declared || path.startsWith(base);
6085
+ }
6086
+ function driftWarning(message, remediation) {
6087
+ return diagnostic({
6088
+ severity: "warning",
6089
+ code: "provider-capabilities-drift",
6090
+ subject: "harnesstrim",
6091
+ message,
6092
+ remediation
6093
+ });
6094
+ }
6095
+ function formatSurface(surface) {
6096
+ return `${surface.toolFamily}/${surface.interceptionPoint}`;
6097
+ }
6098
+ function narrowable(capabilities) {
6099
+ if (capabilities === null || capabilities === void 0)
6100
+ return false;
6101
+ const managed = MANIFEST4.harnesses.map((entry) => entry.harness);
6102
+ return managed.every((harness) => {
6103
+ const observed = capabilities.harnesses[harness];
6104
+ return observed !== void 0 && observed.narrowing.length > 0;
6105
+ });
6106
+ }
6107
+ function writeSetStillReviewed(capabilities, harness) {
6108
+ const review = MANIFEST4.delegatedInstallReview;
6109
+ if (review === null || capabilities === null)
6110
+ return false;
6111
+ const observed = capabilities.harnesses[harness];
6112
+ if (observed === void 0)
6113
+ return false;
6114
+ const declared = observed.writeSet.map(writeSetPath);
6115
+ const boundary = review.containmentBoundary.map(writeSetPath);
6116
+ const everyDeclaredPathContained = declared.every((entry) => boundary.some((prefix) => coveredBy(entry, prefix)));
6117
+ const everyReviewedPathCovered = review.reviewedWriteSet.every((reviewed) => declared.some((entry) => coveredBy(reviewed, entry)));
6118
+ return everyDeclaredPathContained && everyReviewedPathCovered;
6119
+ }
6120
+ function compareCapabilities(manifest, capabilities) {
6121
+ const warnings = [];
6122
+ for (const declaration of manifest.capabilities) {
6123
+ for (const harness of declaration.harnesses) {
6124
+ const observed = capabilities.harnesses[harness];
6125
+ if (observed === void 0) {
6126
+ warnings.push(driftWarning(`the manifest declares ${declaration.capability} on ${harness}, but \`harnesstrim capabilities\` (${capabilities.version}) lists no ${harness} entry`, `Re-test the ${harness} integration: either the manifest is stale or ${harness} lost its HarnessTrim surface`));
6127
+ continue;
6128
+ }
6129
+ const matched = declaration.surfaces.some((surface) => observed.surfaces.some((entry) => surfaceMatchesDeclaration(entry, surface)));
6130
+ if (!matched) {
6131
+ warnings.push(driftWarning(`the manifest records ${declaration.capability} on ${harness} over ${declaration.surfaces.map(formatSurface).join(", ")}, but \`harnesstrim capabilities\` (${capabilities.version}) reports ${observed.surfaces.length === 0 ? "no surfaces" : observed.surfaces.join(" | ")}`, `Re-test the ${harness} integration and update the manifest declaration`));
6132
+ }
6133
+ }
6134
+ }
6135
+ const review = manifest.delegatedInstallReview;
6136
+ if (review === null)
6137
+ return warnings;
6138
+ const claude = capabilities.harnesses["claude"];
6139
+ if (claude === void 0)
6140
+ return warnings;
6141
+ const declared = claude.writeSet.map(writeSetPath);
6142
+ for (const reviewed of review.reviewedWriteSet) {
6143
+ if (declared.some((entry) => coveredBy(reviewed, entry)))
6144
+ continue;
6145
+ warnings.push(driftWarning(`the reviewed write set records ${reviewed} at ${review.upstreamVersion}, but \`harnesstrim capabilities\` (${capabilities.version}) declares nothing covering it (${declared.length === 0 ? "nothing" : declared.join(", ")})`, `Re-review the write set at the installed version before delegating another install`));
6146
+ }
6147
+ const boundary = review.containmentBoundary.map(writeSetPath);
6148
+ for (const entry of declared) {
6149
+ if (boundary.some((prefix) => coveredBy(entry, prefix)))
6150
+ continue;
6151
+ warnings.push(driftWarning(`\`harnesstrim capabilities\` (${capabilities.version}) declares ${entry} for claude, which sits outside the containment boundary recorded at ${review.upstreamVersion} (${boundary.join(", ")})`, "Re-review the write set: rollback restores the boundary, and a path it does not cover would survive a rollback"));
6152
+ }
6153
+ return warnings;
5334
6154
  }
5335
6155
  function harnessesWiredToHarnessTrim(configs) {
5336
6156
  const wired = /* @__PURE__ */ new Set();
@@ -5413,16 +6233,17 @@ async function probeExecutable(context) {
5413
6233
  };
5414
6234
  }
5415
6235
  function metricsLocations(context) {
5416
- return [
5417
- context.fs.join(context.projectRoot, ".harnesstrim", "metrics.jsonl"),
5418
- context.fs.join(context.paths.home, ".hermes", "harnesstrim-metrics.jsonl")
5419
- ];
6236
+ return [context.fs.join(context.projectRoot, ".harnesstrim", "metrics.jsonl")];
5420
6237
  }
5421
6238
  async function detect4(context) {
5422
6239
  const probe = await probeExecutable(context);
6240
+ const observed = probe.installed ? await probeCapabilities(context) : null;
5423
6241
  const configured = harnessesWiredToHarnessTrim(context.harnessConfigs);
5424
6242
  const warnings = [];
5425
- const evidenceItems = [...probe.evidence];
6243
+ const evidenceItems = [...probe.evidence, ...observed?.evidence ?? []];
6244
+ if (observed !== null && observed.capabilities !== null) {
6245
+ warnings.push(...compareCapabilities(MANIFEST4, observed.capabilities));
6246
+ }
5426
6247
  for (const harness of configured) {
5427
6248
  evidenceItems.push(evidence({
5428
6249
  kind: "config-entry",
@@ -5460,6 +6281,10 @@ async function detect4(context) {
5460
6281
  // circumstantial: PLAN §11 says Token Harness never installs it, so every installation it ever
5461
6282
  // sees is the user's.
5462
6283
  managedByTokenHarness: false,
6284
+ // PLAN §15 item 46. Decided by the build in front of us, not by the manifest: a build that
6285
+ // declares narrowing flags for every managed harness can be asked for the narrowed states
6286
+ // §6.1 recorded as unproducible, and one that cannot answer keeps the `0.0.5` verdict.
6287
+ assignable: observed?.capabilities === null ? false : narrowable(observed?.capabilities),
5463
6288
  evidence: evidenceItems,
5464
6289
  warnings
5465
6290
  };
@@ -5481,6 +6306,12 @@ function parseTrimEvent(line) {
5481
6306
  if (typeof ts !== "string" || typeof harness !== "string" || typeof before !== "number" || typeof after !== "number") {
5482
6307
  return null;
5483
6308
  }
6309
+ const schemaVersion = typeof record["schemaVersion"] === "number" ? record["schemaVersion"] : 0;
6310
+ if (schemaVersion > 1)
6311
+ return null;
6312
+ const eventId = schemaVersion === 1 && typeof record["eventId"] === "string" && record["eventId"] !== "" ? record["eventId"] : null;
6313
+ if (schemaVersion === 1 && eventId === null)
6314
+ return null;
5484
6315
  return {
5485
6316
  ts,
5486
6317
  harness,
@@ -5488,6 +6319,11 @@ function parseTrimEvent(line) {
5488
6319
  reducer: typeof record["reducer"] === "string" ? record["reducer"] : null,
5489
6320
  beforeChars: before,
5490
6321
  afterChars: after,
6322
+ schemaVersion,
6323
+ eventId,
6324
+ changed: typeof record["changed"] === "boolean" ? record["changed"] : null,
6325
+ beforeTokens: typeof record["beforeTokens"] === "number" ? record["beforeTokens"] : null,
6326
+ afterTokens: typeof record["afterTokens"] === "number" ? record["afterTokens"] : null,
5491
6327
  ...typeof record["mode"] === "string" ? { mode: record["mode"] } : {}
5492
6328
  };
5493
6329
  }
@@ -5499,16 +6335,18 @@ function toEvent(event, sourceId, ordinal, line, context) {
5499
6335
  const instant = new Date(event.ts);
5500
6336
  if (Number.isNaN(instant.getTime()))
5501
6337
  return null;
6338
+ const native = event.schemaVersion === 1;
5502
6339
  const dryrun = event.mode === "dryrun";
5503
- const eventId = synthesizeEventId(sourceId, ordinal, line);
6340
+ const eventId = native ? event.eventId : synthesizeEventId(sourceId, ordinal, line);
6341
+ const passThrough = native && event.changed === false;
6342
+ const hasTokens = native && event.beforeTokens !== null && event.afterTokens !== null;
6343
+ const hasUnprovenMode = native && MODE_CARRYING_HARNESSES.has(event.harness) && !hasTokens && !passThrough;
5504
6344
  return {
5505
6345
  schemaVersion: OPTIMIZATION_EVENT_SCHEMA_VERSION,
5506
6346
  eventId,
5507
6347
  timestamp: instant.toISOString(),
5508
6348
  provider: { id: HARNESSTRIM2, version: null },
5509
6349
  context: {
5510
- // The project the metrics file belongs to, which for the default location is the project it
5511
- // sits inside.
5512
6350
  projectId: context.projectIdFor(context.projectRoot),
5513
6351
  // Unlike RTK's database, a `TrimEvent` names its harness, so this is read rather than left
5514
6352
  // unknown.
@@ -5521,25 +6359,34 @@ function toEvent(event, sourceId, ordinal, line, context) {
5521
6359
  capability: event.harness === OPENCODE2 ? "tool.output.reduce" : "shell.output.reduce"
5522
6360
  },
5523
6361
  measurement: {
5524
- class: dryrun ? "counterfactual" : "estimated-local",
6362
+ class: dryrun || hasUnprovenMode ? "counterfactual" : hasTokens ? "exact-local" : "estimated-local",
5525
6363
  beforeChars: event.beforeChars,
5526
6364
  afterChars: event.afterChars,
5527
- // RFC 0005: "never derived silently". `0.0.5` counts characters and nothing else.
5528
- beforeTokens: null,
5529
- afterTokens: null,
6365
+ // RFC 0005: "never derived silently". `0.0.5` counts characters and nothing else; the
6366
+ // `0.1.0` producer's counts are used as they are because a token count is a token count
6367
+ // where one exists.
6368
+ beforeTokens: hasTokens ? event.beforeTokens : null,
6369
+ afterTokens: hasTokens ? event.afterTokens : null,
5530
6370
  tokenizer: null,
5531
6371
  confidenceLow: null,
5532
6372
  confidenceHigh: null
5533
6373
  },
5534
6374
  outcome: {
5535
- // A dryrun leaves `output.output` untouched, so nothing the model saw changed.
5536
- changed: !dryrun && event.afterChars !== event.beforeChars,
5537
- bypassReason: dryrun ? "dryrun" : event.afterChars === event.beforeChars ? "no-reduction-applied" : null,
6375
+ // A dryrun leaves `output.output` untouched; a native line states whether its attempt
6376
+ // actually changed the output. A native line whose mode the envelope cannot carry is
6377
+ // deliberately not asserted as a change: the figure may describe a dryrun, and reporting
6378
+ // `changed: true` would claim the model received fewer bytes than it did.
6379
+ changed: dryrun || hasUnprovenMode ? false : native ? event.changed ?? true : event.afterChars !== event.beforeChars,
6380
+ bypassReason: dryrun ? "dryrun" : (
6381
+ // Same reason value as the diagnostic code for the residual ambiguity: nothing refutes a
6382
+ // dryrun, and nothing proves one, so the bypass is the envelope's own silence.
6383
+ hasUnprovenMode ? "mode-unresolved" : native ? passThrough ? "pass-through" : null : event.afterChars === event.beforeChars ? "no-reduction-applied" : null
6384
+ ),
5538
6385
  originalReference: null,
5539
6386
  latencyMs: null,
5540
6387
  errorCode: null
5541
6388
  },
5542
- source: { nativeEventId: null, importedAt: context.now() }
6389
+ source: { nativeEventId: native ? eventId : null, importedAt: context.now() }
5543
6390
  };
5544
6391
  }
5545
6392
  async function verify4(context) {
@@ -5590,7 +6437,7 @@ async function verify4(context) {
5590
6437
  const agents = context.fs.join(context.projectRoot, "AGENTS.md");
5591
6438
  if (await context.fs.stat(agents) !== null) {
5592
6439
  const text = new TextDecoder().decode(await context.fs.readFile(agents));
5593
- if (identifiesCommand(text)) {
6440
+ if (HOOK_COMMAND_PATTERN.test(text)) {
5594
6441
  checks.push({
5595
6442
  id: "instruction-path-present",
5596
6443
  status: "info",
@@ -5626,6 +6473,8 @@ async function collectMetrics(context, store) {
5626
6473
  let skipped = 0;
5627
6474
  let lastCursor = null;
5628
6475
  let readAny = false;
6476
+ let sawNative = false;
6477
+ let unprovableModeEvents = 0;
5629
6478
  for (const path of metricsLocations(context)) {
5630
6479
  const stat3 = await context.fs.stat(path);
5631
6480
  if (stat3 === null)
@@ -5676,6 +6525,10 @@ async function collectMetrics(context, store) {
5676
6525
  ordinal += 1;
5677
6526
  continue;
5678
6527
  }
6528
+ if (parsed.schemaVersion === 1)
6529
+ sawNative = true;
6530
+ if (event.outcome.bypassReason === "mode-unresolved")
6531
+ unprovableModeEvents += 1;
5679
6532
  events.push(event);
5680
6533
  lastLine = trimmed;
5681
6534
  ordinal += 1;
@@ -5727,12 +6580,26 @@ async function collectMetrics(context, store) {
5727
6580
  diagnostics
5728
6581
  };
5729
6582
  }
6583
+ if (unprovableModeEvents > 0) {
6584
+ diagnostics.push(diagnostic({
6585
+ severity: "info",
6586
+ code: "provider-metrics-mode-unresolved",
6587
+ message: `${String(unprovableModeEvents)} native event${unprovableModeEvents === 1 ? "" : "s"} from a mode-carrying harness could not be classed as realized: schema 1 carries no reduction mode`,
6588
+ remediation: "Treat these figures as counterfactual; only OpenCode can later prove its mode from the configured plugin option"
6589
+ }));
6590
+ }
5730
6591
  return {
5731
6592
  providerId: HARNESSTRIM2,
5732
- // RFC 0005 §Importer degradation policy: character-only events are `legacy`, and "running in
5733
- // legacy mode is a supported steady state, not a warning".
5734
- mode: "legacy",
5735
- source: `harnesstrim TrimEvent JSONL (${TESTED_UPSTREAM} shape)`,
6593
+ // RFC 0005 §Importer degradation policy, decided by what this run read: `native` when the
6594
+ // file carried schema 1 events with producer IDs, `legacy` when it only had the character
6595
+ // stream. Legacy inside a native run is not a problem — those lines are older files.
6596
+ //
6597
+ // A native stream whose mode-carrying harness lines cannot prove a realized reduction (no
6598
+ // tokens, no pass-through) is `native-with-residue`: the import is native, but a part of it
6599
+ // could not be classed as realized, and the count above qualifies exactly how much. A bare
6600
+ // `native` must keep meaning "everything was classed".
6601
+ mode: sawNative && unprovableModeEvents > 0 ? "native-with-residue" : sawNative ? "native" : "legacy",
6602
+ source: `harnesstrim TrimEvent JSONL (schema 0 and schema 1)`,
5736
6603
  imported,
5737
6604
  skipped,
5738
6605
  cursor: lastCursor,
@@ -5769,7 +6636,12 @@ async function plan(context, request) {
5769
6636
  return { providerId: HARNESSTRIM2, desiredState: "absent", actions };
5770
6637
  }
5771
6638
  const installed = await probeExecutable(context);
5772
- if (installed.version !== TESTED_UPSTREAM) {
6639
+ if (!installed.installed) {
6640
+ return { providerId: HARNESSTRIM2, desiredState: "configured", actions: [] };
6641
+ }
6642
+ const observed = await probeCapabilities(context);
6643
+ const reviewed = observed.capabilities === null ? installed.version === TESTED_UPSTREAM : writeSetStillReviewed(observed.capabilities, CLAUDE2);
6644
+ if (!reviewed) {
5773
6645
  return { providerId: HARNESSTRIM2, desiredState: "configured", actions: [] };
5774
6646
  }
5775
6647
  const review = MANIFEST4.delegatedInstallReview;
@@ -5819,6 +6691,7 @@ var harnesstrimAdapter = {
5819
6691
 
5820
6692
  // packages/adapters/dist/src/providers/rtk-plan.js
5821
6693
  var RTK_HOOK_COMMAND_PREFIX = "rtk hook";
6694
+ var HOOK_LIST_HARNESSES = /* @__PURE__ */ new Set(["claude"]);
5822
6695
  function hookListPointer(eventName) {
5823
6696
  return `hooks.${eventName.replace(/\./g, "\\.")}`;
5824
6697
  }
@@ -5834,6 +6707,8 @@ function planTargets(context, request) {
5834
6707
  const harness = request.harnesses.find((entry) => entry.id === owned.scope.harness);
5835
6708
  if (harness === void 0)
5836
6709
  continue;
6710
+ if (!HOOK_LIST_HARNESSES.has(harness.id))
6711
+ continue;
5837
6712
  const point = harness.interceptionPoints.find((entry) => entry.scopeId === owned.scope.interceptionPoint);
5838
6713
  if (point === void 0)
5839
6714
  continue;
@@ -5966,11 +6841,11 @@ function installAction(context, manifest) {
5966
6841
  channel.digestAvailable ? "the release artifact carries a digest that will be verified" : "this channel publishes no digest, so the artifact cannot be verified by content"
5967
6842
  ],
5968
6843
  postconditions: ["`rtk --version` reports a version inside the tested range"],
5969
- // Not `package-inventory`: that promises an inventory-based reversal nothing implements. RFC
5970
- // 0004 allows rollback only by restoring snapshots, "never by inventing an uninstall command",
5971
- // and a package is not a file — so an installed package survives a rollback and the executor
5972
- // says so rather than letting the report imply otherwise.
5973
- rollbackData: "none",
6844
+ // The declared value the executor now implements (RFC 0009 §Initial delivery order item 1):
6845
+ // the executor captures whatever the channel has installed before the install runs, and a
6846
+ // rollback restores that captured version through the same channel. `none` is left only for
6847
+ // channels this build cannot ask about — there the executor says the package stayed instead.
6848
+ rollbackData: channelCanReportInventory(channel.id) ? "package-inventory" : "none",
5974
6849
  explanation: `Install RTK through ${channel.id}`,
5975
6850
  packageManager: channel.id,
5976
6851
  // The channel's own name for the package, because they differ: `rtk-ai.rtk` on winget and
@@ -6009,6 +6884,7 @@ function buildRtkPlan(input) {
6009
6884
  // packages/adapters/dist/src/providers/rtk.js
6010
6885
  var RTK2 = providerId("rtk");
6011
6886
  var CLAUDE3 = harnessId("claude");
6887
+ var OPENCODE3 = harnessId("opencode");
6012
6888
  var MANIFEST5 = {
6013
6889
  schemaVersion: MANIFEST_SCHEMA_VERSION,
6014
6890
  id: RTK2,
@@ -6047,10 +6923,53 @@ var MANIFEST5 = {
6047
6923
  sourceReference: "docs/spikes/2.5-live-verification-log.md",
6048
6924
  upstreamVersion: "0.42.0"
6049
6925
  }
6926
+ },
6927
+ /**
6928
+ * OpenCode, from spike 9.1.
6929
+ *
6930
+ * Separate entries rather than adding `OPENCODE` to the two above, because the surface is not
6931
+ * the same one: RTK reaches Claude Code through a `PreToolUse` hook matching `Bash`, and
6932
+ * OpenCode through a plugin on `tool.execute.before`. A capability entry names one surface
6933
+ * list, and merging these would claim that RTK serves `Bash/pre-tool-use` on OpenCode — a
6934
+ * point OpenCode does not have.
6935
+ *
6936
+ * `shell.command.rewrite` and `shell.output.reduce` again arrive together, for the reason they
6937
+ * do on Claude Code: the plugin calls `rtk rewrite`, and the rewritten command is what filters
6938
+ * its own output. One interception point, two capabilities.
6939
+ */
6940
+ {
6941
+ capability: "shell.command.rewrite",
6942
+ mode: "exclusive",
6943
+ harnesses: [OPENCODE3],
6944
+ // `tool.execute` is the family the harness manifest declares as shell-executing. The plugin
6945
+ // narrows to `tool === "bash" || tool === "shell"` inside that family, which is every shell
6946
+ // route OpenCode exposes — unlike Claude Code on Windows, there is no second one to miss.
6947
+ surfaces: [{ toolFamily: "tool.execute", interceptionPoint: "tool-execute-before" }],
6948
+ evidence: {
6949
+ sourceReference: "docs/spikes/9.1-rtk-opencode-observation-log.md",
6950
+ upstreamVersion: "0.44.0"
6951
+ }
6952
+ },
6953
+ {
6954
+ capability: "shell.output.reduce",
6955
+ mode: "exclusive",
6956
+ harnesses: [OPENCODE3],
6957
+ surfaces: [{ toolFamily: "tool.execute", interceptionPoint: "tool-execute-before" }],
6958
+ evidence: {
6959
+ sourceReference: "docs/spikes/9.1-rtk-opencode-observation-log.md",
6960
+ upstreamVersion: "0.44.0"
6961
+ }
6050
6962
  }
6051
6963
  ],
6052
6964
  platforms: [
6053
- { os: "windows", wsl: false, supported: true, limitation: null },
6965
+ {
6966
+ os: "windows",
6967
+ wsl: false,
6968
+ supported: true,
6969
+ // Spike 9.1. Named on Windows alone because Windows is where Desktop was run; the same
6970
+ // defect on another platform's Desktop build would be a guess until someone watches it.
6971
+ limitation: "RTK's OpenCode plugin is inert under OpenCode Desktop, which supplies no `$` shell helper to plugins, so the plugin's own guard disables it silently"
6972
+ },
6054
6973
  { os: "linux", wsl: true, supported: true, limitation: null },
6055
6974
  { os: "macos", wsl: false, supported: true, limitation: null },
6056
6975
  { os: "linux", wsl: false, supported: true, limitation: null }
@@ -6060,6 +6979,31 @@ var MANIFEST5 = {
6060
6979
  harness: CLAUDE3,
6061
6980
  testedVersions: { minimum: "2.0.0", maximum: "2.1.212" },
6062
6981
  verificationTier: "canary"
6982
+ },
6983
+ {
6984
+ harness: OPENCODE3,
6985
+ // Spike 9.1 watched interception on the CLI at `1.18.11` and watched it fail on Desktop at
6986
+ // `1.18.14`. The range covers both because both were observed; the failure is a declared
6987
+ // platform limitation below rather than a narrower range, because it is a property of the
6988
+ // host application and not of the OpenCode version — the same 1.18.x plugin loads in both.
6989
+ testedVersions: { minimum: "1.18.11", maximum: "1.18.14" },
6990
+ /**
6991
+ * `config-only`, even though spike 9.1 watched a canary-grade interception here — the counter
6992
+ * moved by exactly the number of shell calls the OpenCode session made.
6993
+ *
6994
+ * A tier is what `verify` can prove on the user's machine, not what a spike proved once on a
6995
+ * clean one. RTK's receipt is its history database, and the `commands` table carries
6996
+ * `timestamp`, token counts, `exec_time_ms`, and `project_path` — no harness column, and no
6997
+ * column standing in for one. The spike could attribute those rows because it ran OpenCode
6998
+ * alone in a scratch directory and read the counter either side; `verify` runs where both
6999
+ * harnesses are wired to the same binary and writing into the same table, and cannot say
7000
+ * which of them a row came from.
7001
+ *
7002
+ * Declaring `canary` here would credit RTK's whole command history to whichever harness the
7003
+ * reader happened to be looking at. Claude Code keeps `canary` because its own hook receipt
7004
+ * is per-harness; this one is provider-wide, and RFC 0007 asks the question per harness.
7005
+ */
7006
+ verificationTier: "config-only"
6063
7007
  }
6064
7008
  ],
6065
7009
  installationChannels: [
@@ -6103,9 +7047,10 @@ var MANIFEST5 = {
6103
7047
  },
6104
7048
  delegatedInstallReview: null
6105
7049
  };
6106
- var TESTED_VERSIONS2 = { minimum: "0.40.0", maximum: "0.42.0" };
7050
+ var TESTED_VERSIONS2 = { minimum: "0.40.0", maximum: "0.44.0" };
6107
7051
  var VERSION_PATTERN5 = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/;
6108
7052
  var HOOK_COMMAND_PATTERN2 = /(^|[\\/\s"'])rtk(\.exe)?([\s"']|$)/i;
7053
+ var PLUGIN_MODULE_PATTERN3 = /(^|[\\/])rtk\.(ts|js|mjs|cjs|mts|cts)$/i;
6109
7054
  function isRecord4(value) {
6110
7055
  return typeof value === "object" && value !== null && !Array.isArray(value);
6111
7056
  }
@@ -6148,7 +7093,7 @@ function parseRtkAnalytics(text) {
6148
7093
  function harnessesWiredToRtk(configs) {
6149
7094
  const wired = /* @__PURE__ */ new Set();
6150
7095
  for (const config of configs) {
6151
- if (config.commands.some((command) => HOOK_COMMAND_PATTERN2.test(command))) {
7096
+ if (config.commands.some((command) => identifiesCommand2(command))) {
6152
7097
  wired.add(config.harnessId);
6153
7098
  }
6154
7099
  }
@@ -6264,13 +7209,32 @@ async function detect5(context) {
6264
7209
  versionVerdict: version2.verdict,
6265
7210
  configuredHarnesses: configured,
6266
7211
  unmanagedHarnessesConfigured: [],
6267
- // RFC 0002 §Providers may exceed the managed surface. RTK's manifest covers Claude
6268
- // Code only, so the qualifier RFC 0006's doctor transcript uses for HarnessTrim does
6269
- // not apply here.
7212
+ /**
7213
+ * RFC 0002 §Providers may exceed the managed surface.
7214
+ *
7215
+ * The field asks whether this manifest names a harness with no adapter behind it. It names two,
7216
+ * Claude Code and OpenCode, and both are managed — so the qualifier RFC 0006's doctor transcript
7217
+ * uses for HarnessTrim does not apply, and the report says "not configured for any harness"
7218
+ * without hedging.
7219
+ *
7220
+ * `rtk init --agent` separately targets Cursor, Windsurf, Cline, Kilo Code, Antigravity, Kimi,
7221
+ * Pi, Hermes, and Droid. That is RTK exceeding *Token Harness*, not this manifest exceeding the
7222
+ * managed set, and it stays out of the flag: claiming those here would make the resolver
7223
+ * responsible for harnesses this build has no adapter to inspect.
7224
+ */
6270
7225
  supportsUnmanagedHarnesses: false,
6271
7226
  // RFC 0004 §Brownfield adoption: nothing has been applied by Token Harness yet, so
6272
7227
  // every installation it finds is the user's.
6273
7228
  managedByTokenHarness: false,
7229
+ /**
7230
+ * Always, and independent of the observed version.
7231
+ *
7232
+ * RTK's assignment is produced by this build writing the hook itself — `rtk-plan.ts` appends
7233
+ * the entry — so nothing about the installed `rtk` decides whether the state is reachable.
7234
+ * That is the difference from HarnessTrim, whose assignment is produced by *its* installer and
7235
+ * therefore depends on which flags that installer has.
7236
+ */
7237
+ assignable: true,
6274
7238
  evidence: evidenceItems,
6275
7239
  warnings
6276
7240
  };
@@ -6392,7 +7356,7 @@ function toEvent2(row2, context) {
6392
7356
  context: {
6393
7357
  // An empty `project_path` means RTK recorded no directory; attributing the event to
6394
7358
  // the directory `metrics` happens to run in would invent an attribution.
6395
- projectId: projectPath === null || projectPath === "" ? "p_unattributed" : context.projectIdFor(projectPath),
7359
+ projectId: projectPath === null || projectPath === "" ? UNATTRIBUTED_PROJECT_ID : context.projectIdFor(projectPath),
6396
7360
  // RTK proxies commands for whichever harness invoked it and does not record which.
6397
7361
  // The hook that wires it is per harness, but a *row* carries no harness, and reading
6398
7362
  // one off the current configuration would attribute months of history to today's
@@ -6582,7 +7546,7 @@ async function collectMetrics2(context, store) {
6582
7546
  }
6583
7547
  var SOURCE_LABEL = "rtk history.db (commands)";
6584
7548
  function identifiesCommand2(command) {
6585
- return HOOK_COMMAND_PATTERN2.test(command);
7549
+ return HOOK_COMMAND_PATTERN2.test(command) || PLUGIN_MODULE_PATTERN3.test(command);
6586
7550
  }
6587
7551
  async function plan2(context, request) {
6588
7552
  const version2 = await readVersion(context);
@@ -6611,7 +7575,6 @@ function listProviderAdapters() {
6611
7575
 
6612
7576
  // apps/cli/dist/src/commands/plan.js
6613
7577
  var DEFAULT_PROFILE = "safe";
6614
- var ASSIGNABLE_PROVIDERS = ["rtk"];
6615
7578
  async function computePlan(context) {
6616
7579
  const diagnostics = [];
6617
7580
  const providerAdapters = listProviderAdapters().filter((adapter) => context.provider === null || adapter.manifest.id === context.provider);
@@ -6638,11 +7601,6 @@ async function computePlan(context) {
6638
7601
  harnessConfigs.push(...inspection.summaries);
6639
7602
  }
6640
7603
  }
6641
- const providers = providerAdapters.map((adapter) => ({
6642
- id: adapter.manifest.id,
6643
- capabilities: adapter.manifest.capabilities,
6644
- assignable: ASSIGNABLE_PROVIDERS.includes(adapter.manifest.id)
6645
- }));
6646
7604
  const providerContext = detectionContext === null || context.adapters === null ? null : {
6647
7605
  ...detectionContext,
6648
7606
  harnessConfigs,
@@ -6650,12 +7608,19 @@ async function computePlan(context) {
6650
7608
  localDatabase: context.adapters.localDatabase,
6651
7609
  projectIdFor: context.adapters.projectIdFor
6652
7610
  };
7611
+ const assignable = /* @__PURE__ */ new Map();
6653
7612
  if (providerContext !== null) {
6654
7613
  for (const adapter of providerAdapters) {
6655
7614
  const detection = await adapter.detect(providerContext);
6656
7615
  versions.providers[adapter.manifest.id] = detection.version;
7616
+ assignable.set(adapter.manifest.id, detection.assignable);
6657
7617
  }
6658
7618
  }
7619
+ const providers = providerAdapters.map((adapter) => ({
7620
+ id: adapter.manifest.id,
7621
+ capabilities: adapter.manifest.capabilities,
7622
+ assignable: assignable.get(adapter.manifest.id) ?? false
7623
+ }));
6659
7624
  const resolution = resolveOwnership({
6660
7625
  profile,
6661
7626
  harnesses: present,
@@ -6665,7 +7630,9 @@ async function computePlan(context) {
6665
7630
  harness: context.harness
6666
7631
  });
6667
7632
  const actions = [];
7633
+ const blocked = [];
6668
7634
  if (providerContext !== null) {
7635
+ const rows = context.compatibilityRows ?? COMPATIBILITY_ROWS;
6669
7636
  for (const adapter of providerAdapters) {
6670
7637
  const owned = resolution.ownership.filter((entry) => entry.owner === adapter.manifest.id);
6671
7638
  if (owned.length === 0 && adapter.plansWithoutOwnership !== true)
@@ -6675,7 +7642,35 @@ async function computePlan(context) {
6675
7642
  harnesses: present,
6676
7643
  desiredState: "configured"
6677
7644
  });
6678
- actions.push(...providerPlan.actions);
7645
+ if (providerPlan.actions.length === 0)
7646
+ continue;
7647
+ const touched = [...new Set(owned.map((entry) => entry.scope.harness))];
7648
+ if (touched.length === 0)
7649
+ touched.push(...present.map((harness) => harness.id));
7650
+ let admitted = true;
7651
+ for (const harness of touched) {
7652
+ const admission = admitManagedMutation(rows, {
7653
+ provider: adapter.manifest.id,
7654
+ providerVersion: versions.providers[adapter.manifest.id] ?? null,
7655
+ harness,
7656
+ harnessVersion: versions.harnesses[harness] ?? null,
7657
+ os: context.platform.os,
7658
+ wsl: context.platform.isWsl
7659
+ });
7660
+ if (admission.state !== "admitted") {
7661
+ admitted = false;
7662
+ blocked.push({
7663
+ provider: adapter.manifest.id,
7664
+ providerVersion: versions.providers[adapter.manifest.id] ?? null,
7665
+ harness,
7666
+ harnessVersion: versions.harnesses[harness] ?? null,
7667
+ verdict: admission.verdict,
7668
+ missing: admission.missing
7669
+ });
7670
+ }
7671
+ }
7672
+ if (admitted)
7673
+ actions.push(...providerPlan.actions);
6679
7674
  }
6680
7675
  }
6681
7676
  const report = {
@@ -6733,7 +7728,7 @@ async function computePlan(context) {
6733
7728
  remediation: null
6734
7729
  }));
6735
7730
  }
6736
- if (resolution.ownership.length > 0 && actions.length === 0) {
7731
+ if (resolution.ownership.length > 0 && actions.length === 0 && blocked.length === 0) {
6737
7732
  diagnostics.push(diagnostic({
6738
7733
  severity: "info",
6739
7734
  code: "already-in-desired-state",
@@ -6741,6 +7736,14 @@ async function computePlan(context) {
6741
7736
  remediation: null
6742
7737
  }));
6743
7738
  }
7739
+ for (const entry of blocked) {
7740
+ diagnostics.push(diagnostic({
7741
+ severity: "error",
7742
+ code: "managed-mutation-blocked",
7743
+ message: `plan refuses managed mutation of ${entry.harness} by ${entry.provider}${entry.providerVersion !== null ? ` at ${entry.providerVersion}` : ""}: ${entry.missing}`,
7744
+ remediation: "Add a compatibility row whose fixture proves this combination, or configure this integration by hand"
7745
+ }));
7746
+ }
6744
7747
  for (const conflict of resolution.conflicts) {
6745
7748
  diagnostics.push(diagnostic({
6746
7749
  severity: "error",
@@ -6749,7 +7752,7 @@ async function computePlan(context) {
6749
7752
  remediation: conflict.remediation
6750
7753
  }));
6751
7754
  }
6752
- return { report, versions, diagnostics, present, harnessConfigs };
7755
+ return { report, versions, diagnostics, present, harnessConfigs, blocked };
6753
7756
  }
6754
7757
  async function persist(context, computed) {
6755
7758
  if (context.adapters === null || context.stateRoot === null)
@@ -6774,7 +7777,7 @@ async function runPlan(context) {
6774
7777
  const computed = await computePlan(context);
6775
7778
  const persisted = await persist(context, computed);
6776
7779
  const report = { ...computed.report, persisted };
6777
- const exitCode = report.conflicts.length > 0 ? EXIT_CODES["blocked-by-conflict"] : EXIT_CODES.ok;
7780
+ const exitCode = computed.blocked.length > 0 ? EXIT_CODES["unsupported-environment"] : report.conflicts.length > 0 ? EXIT_CODES["blocked-by-conflict"] : EXIT_CODES.ok;
6778
7781
  return commandResult({
6779
7782
  command: "plan",
6780
7783
  exitCode,
@@ -6845,6 +7848,9 @@ async function runApply(context) {
6845
7848
  const diagnostics = [];
6846
7849
  const computed = await computePlan(context);
6847
7850
  diagnostics.push(...computed.diagnostics);
7851
+ if (computed.blocked.length > 0) {
7852
+ return finish("rejected", EXIT_CODES["unsupported-environment"], emptyReport("rejected"), diagnostics);
7853
+ }
6848
7854
  if (computed.report.conflicts.length > 0) {
6849
7855
  return finish("rejected", EXIT_CODES["blocked-by-conflict"], emptyReport("rejected"), diagnostics);
6850
7856
  }
@@ -6999,10 +8005,40 @@ async function runDoctor(context) {
6999
8005
  projectIdFor: context.adapters?.projectIdFor ?? (() => "p_unattributed")
7000
8006
  })));
7001
8007
  const problemCount = harnesses.filter(isBroken).length + providers.filter(isBroken).length + [...harnesses, ...providers].filter((detection) => detection.versionVerdict === "unknown-newer").length;
8008
+ const rows = context.compatibilityRows ?? COMPATIBILITY_ROWS;
8009
+ const providersWithCoverage = providers.map((detection) => {
8010
+ if (detection.state !== "configured" || detection.configuredHarnesses.length === 0) {
8011
+ return detection;
8012
+ }
8013
+ const uncovered = detection.configuredHarnesses.flatMap((harness) => {
8014
+ const harnessVersion = harnesses.find((candidate) => candidate.harnessId === harness)?.version ?? null;
8015
+ const admission = admitManagedMutation(rows, {
8016
+ provider: detection.providerId,
8017
+ providerVersion: detection.version,
8018
+ harness,
8019
+ harnessVersion,
8020
+ os: context.platform.os,
8021
+ wsl: context.platform.isWsl
8022
+ });
8023
+ if (admission.state === "admitted")
8024
+ return [];
8025
+ return [
8026
+ diagnostic({
8027
+ severity: "warning",
8028
+ code: "no-compatibility-row",
8029
+ message: `no compatibility row covers ${detection.providerId} on ${harness}${detection.version !== null ? ` at ${detection.version}` : ""} \u2014 ${admission.missing}`,
8030
+ remediation: "Add a compatibility row whose fixture proves this combination, or configure this integration by hand"
8031
+ })
8032
+ ];
8033
+ });
8034
+ if (uncovered.length === 0)
8035
+ return detection;
8036
+ return { ...detection, warnings: [...detection.warnings, ...uncovered] };
8037
+ });
7002
8038
  const report = {
7003
8039
  platform: context.platform,
7004
8040
  harnesses,
7005
- providers,
8041
+ providers: providersWithCoverage,
7006
8042
  problemCount
7007
8043
  };
7008
8044
  return commandResult({
@@ -7094,14 +8130,42 @@ async function runMetrics(context) {
7094
8130
  }
7095
8131
  }
7096
8132
  }
8133
+ const projectId = context.adapters === null ? null : context.adapters.projectIdFor(context.projectRoot);
7097
8134
  const events = [];
7098
8135
  for await (const event of store.query({
7099
8136
  since: window.sinceInstant,
7100
8137
  until: window.untilInstant,
8138
+ ...projectId === null ? {} : { projectId },
7101
8139
  ...context.provider === null ? {} : { providerIds: [context.provider] }
7102
8140
  })) {
7103
8141
  events.push(event);
7104
8142
  }
8143
+ if (projectId === null) {
8144
+ diagnostics.push(diagnostic({
8145
+ severity: "info",
8146
+ code: "metrics-not-project-scoped",
8147
+ message: "This report covers every project in the store: no project identity was available",
8148
+ remediation: null
8149
+ }));
8150
+ } else {
8151
+ let unattributed = 0;
8152
+ for await (const _event of store.query({
8153
+ since: window.sinceInstant,
8154
+ until: window.untilInstant,
8155
+ projectId: UNATTRIBUTED_PROJECT_ID,
8156
+ ...context.provider === null ? {} : { providerIds: [context.provider] }
8157
+ })) {
8158
+ unattributed += 1;
8159
+ }
8160
+ if (unattributed > 0) {
8161
+ diagnostics.push(diagnostic({
8162
+ severity: "info",
8163
+ code: "metrics-unattributed-excluded",
8164
+ message: `${String(unattributed)} operation${unattributed === 1 ? "" : "s"} named no project and are excluded from this report`,
8165
+ remediation: null
8166
+ }));
8167
+ }
8168
+ }
7105
8169
  const report = aggregateEvents({
7106
8170
  events,
7107
8171
  windowStart: window.windowStart,
@@ -7226,7 +8290,12 @@ async function runRollback(context) {
7226
8290
  fs,
7227
8291
  snapshots: built.store,
7228
8292
  journal: built.journal,
7229
- now: context.now
8293
+ now: context.now,
8294
+ // Package inventories, when the transaction captured any: restoring a package needs the same
8295
+ // process runner the install used. Absent, the rollback reports those packages as not
8296
+ // restored instead of attempting a silent reinstall.
8297
+ runner: context.adapters.runner,
8298
+ cwd: context.projectRoot
7230
8299
  });
7231
8300
  diagnostics.push(...result.diagnostics);
7232
8301
  const report = empty(result.exitCode === 0 ? "rolled-back" : "dirty");
@@ -7348,7 +8417,6 @@ async function runUninstall(context) {
7348
8417
  }
7349
8418
 
7350
8419
  // apps/cli/dist/src/commands/status.js
7351
- var ASSIGNABLE_PROVIDERS2 = ["rtk"];
7352
8420
  async function runStatus(context) {
7353
8421
  const report = {
7354
8422
  platform: context.platform,
@@ -7386,12 +8454,8 @@ async function runStatus(context) {
7386
8454
  configs.push(...inspection.summaries);
7387
8455
  }
7388
8456
  const providerAdapters = listProviderAdapters();
7389
- const providers = providerAdapters.map((adapter) => ({
7390
- id: adapter.manifest.id,
7391
- capabilities: adapter.manifest.capabilities,
7392
- assignable: ASSIGNABLE_PROVIDERS2.includes(adapter.manifest.id)
7393
- }));
7394
8457
  const observedVersions = {};
8458
+ const providers = [];
7395
8459
  for (const adapter of providerAdapters) {
7396
8460
  const detection = await adapter.detect({
7397
8461
  ...detectionContext,
@@ -7401,6 +8465,11 @@ async function runStatus(context) {
7401
8465
  projectIdFor: context.adapters.projectIdFor
7402
8466
  });
7403
8467
  observedVersions[adapter.manifest.id] = detection.version;
8468
+ providers.push({
8469
+ id: adapter.manifest.id,
8470
+ capabilities: adapter.manifest.capabilities,
8471
+ assignable: detection.assignable
8472
+ });
7404
8473
  }
7405
8474
  const resolution = resolveOwnership({
7406
8475
  profile: "safe",
@@ -7461,14 +8530,15 @@ function upgradeAction(input) {
7461
8530
  ],
7462
8531
  postconditions: [`${input.packageName} reports ${input.target}`],
7463
8532
  /**
7464
- * `none`, and the consequence is worth stating where a reviewer sees it.
8533
+ * `package-inventory` where the channel can report what is installed, `none` otherwise.
7465
8534
  *
7466
8535
  * An update replaces a binary. Rolling the transaction back restores files and leaves the new
7467
- * version in place, so the executor's `install-not-reversible` diagnostic is not boilerplate
7468
- * here — it is the difference between the machine being restored and the *configuration*
7469
- * being restored.
8536
+ * version in place, so the executor's receipt is not boilerplate here — it is the difference
8537
+ * between the machine being restored and the *configuration* being restored. With an
8538
+ * inventory-capturing channel the rollback also reinstalls the previous version, and the
8539
+ * receipt says per package whether that reinstall took.
7470
8540
  */
7471
- rollbackData: "none",
8541
+ rollbackData: channelCanReportInventory(input.channel) ? "package-inventory" : "none",
7472
8542
  explanation: `Update ${input.packageName} from ${input.installed} to ${input.target} through ${input.channel}`,
7473
8543
  packageManager: input.channel,
7474
8544
  packageName: input.packageName,
@@ -7741,11 +8811,12 @@ async function runVerify(context) {
7741
8811
  presentHarnesses
7742
8812
  );
7743
8813
  for (const harnessId2 of harnesses) {
8814
+ const declaredTier = adapter.manifest.harnesses.find((entry) => entry.harness === harnessId2)?.verificationTier ?? verification.declaredTier;
7744
8815
  results.push({
7745
8816
  providerId: adapter.manifest.id,
7746
8817
  harnessId: harnessId2,
7747
- status: statusFor(verification.achievedTier, verification.declaredTier),
7748
- declaredTier: verification.declaredTier,
8818
+ status: statusFor(verification.achievedTier, declaredTier),
8819
+ declaredTier,
7749
8820
  managedByTokenHarness: MANAGED_PROVIDERS.includes(adapter.manifest.id),
7750
8821
  checks: verification.checks
7751
8822
  });
@@ -7942,7 +9013,8 @@ function renderApplyReport(report, context, command = "apply") {
7942
9013
  lines.push(` ${displayPath(path, context.home)}`);
7943
9014
  lines.push("");
7944
9015
  }
7945
- lines.push((OUTCOME_LINES[command] ?? OUTCOME_LINES["apply"])?.[report.outcome] ?? "");
9016
+ const removedNothing = command === "uninstall" && report.outcome === "committed" && report.results.length > 0 && report.results.every((result) => result.status !== "applied");
9017
+ lines.push(removedNothing ? "Removed nothing: no owned entry was recognised. Everything is untouched." : (OUTCOME_LINES[command] ?? OUTCOME_LINES["apply"])?.[report.outcome] ?? "");
7946
9018
  return document(lines);
7947
9019
  }
7948
9020
 
@@ -8609,7 +9681,7 @@ function usageText(topic) {
8609
9681
  }
8610
9682
 
8611
9683
  // apps/cli/dist/src/version.js
8612
- var TOOL_VERSION = "0.1.1";
9684
+ var TOOL_VERSION = "0.1.2";
8613
9685
 
8614
9686
  // apps/cli/dist/src/run.js
8615
9687
  var DEFAULT_COMMANDS = {
@@ -8786,7 +9858,8 @@ async function run(options) {
8786
9858
  until: invocation.options.until,
8787
9859
  planId: invocation.options.plan,
8788
9860
  confirmed: invocation.options.yes,
8789
- metrics: options.metrics ?? null
9861
+ metrics: options.metrics ?? null,
9862
+ compatibilityRows: options.compatibilityRows ?? null
8790
9863
  };
8791
9864
  const table = options.commands ?? DEFAULT_COMMANDS;
8792
9865
  let result;