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/README.md +89 -12
- package/package.json +1 -1
- package/sbom.json +2 -2
- package/token-harness.mjs +1163 -90
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: "
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5008
|
-
*
|
|
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.
|
|
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: [
|
|
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
|
-
|
|
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
|
|
5315
|
-
//
|
|
5316
|
-
//
|
|
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: "
|
|
5319
|
-
|
|
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
|
-
|
|
5529
|
-
|
|
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
|
|
5536
|
-
changed
|
|
5537
|
-
|
|
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 (
|
|
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
|
|
5733
|
-
//
|
|
5734
|
-
|
|
5735
|
-
|
|
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.
|
|
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
|
-
//
|
|
5970
|
-
//
|
|
5971
|
-
//
|
|
5972
|
-
//
|
|
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
|
-
{
|
|
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.
|
|
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) =>
|
|
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
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
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 === "" ?
|
|
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
|
-
|
|
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
|
-
* `
|
|
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
|
|
7468
|
-
*
|
|
7469
|
-
*
|
|
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,
|
|
7748
|
-
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
|
-
|
|
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.
|
|
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;
|