token-harness 0.1.1 → 0.1.3
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 +117 -16
- package/package.json +1 -1
- package/sbom.json +2 -2
- package/token-harness.mjs +1873 -277
package/token-harness.mjs
CHANGED
|
@@ -97,18 +97,138 @@ function compareVersions(a, b) {
|
|
|
97
97
|
return comparePrerelease(a.prerelease, b.prerelease);
|
|
98
98
|
}
|
|
99
99
|
function classifyVersion(observed, range) {
|
|
100
|
-
const
|
|
100
|
+
const version3 = parseSemanticVersion(observed);
|
|
101
101
|
const minimum = parseSemanticVersion(range.minimum);
|
|
102
|
-
if (
|
|
102
|
+
if (version3 === null || minimum === null)
|
|
103
103
|
return "unparseable";
|
|
104
|
-
if (compareVersions(
|
|
104
|
+
if (compareVersions(version3, minimum) < 0)
|
|
105
105
|
return "below-range";
|
|
106
106
|
if (range.maximum === null)
|
|
107
107
|
return "in-range";
|
|
108
108
|
const maximum = parseSemanticVersion(range.maximum);
|
|
109
109
|
if (maximum === null)
|
|
110
110
|
return "unparseable";
|
|
111
|
-
return compareVersions(
|
|
111
|
+
return compareVersions(version3, maximum) > 0 ? "unknown-newer" : "in-range";
|
|
112
|
+
}
|
|
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
|
+
harness: "codex",
|
|
140
|
+
harnessVersion: { minimum: "0.146.0", maximum: "0.146.0" },
|
|
141
|
+
provider: "harnesstrim",
|
|
142
|
+
providerVersion: "0.1.0",
|
|
143
|
+
platform: { os: "windows", wsl: false, supported: true, limitation: null },
|
|
144
|
+
// The same shape as the Claude skills row, and for the same reason: the install writes skill
|
|
145
|
+
// files rather than a document this build parses, so the schema it depends on is the directory
|
|
146
|
+
// layout. A separate id because the directory is `.codex/skills` and the protected paths differ.
|
|
147
|
+
configSchema: "codex-skills-directory",
|
|
148
|
+
fixture: "tests/fixtures/rows/harnesstrim-codex-windows",
|
|
149
|
+
verificationTier: "config-only"
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
function classifyRowHarnessVersion(rows, observed) {
|
|
153
|
+
if (rows.length === 0)
|
|
154
|
+
return null;
|
|
155
|
+
const parsed = parseSemanticVersion(observed);
|
|
156
|
+
if (parsed === null)
|
|
157
|
+
return null;
|
|
158
|
+
let lowestMinimum = null;
|
|
159
|
+
let highestMaximum = null;
|
|
160
|
+
for (const row2 of rows) {
|
|
161
|
+
const minimum = parseSemanticVersion(row2.harnessVersion.minimum);
|
|
162
|
+
const maximum = parseSemanticVersion(row2.harnessVersion.maximum);
|
|
163
|
+
if (minimum === null || maximum === null)
|
|
164
|
+
continue;
|
|
165
|
+
if (compareVersions(parsed, minimum) >= 0 && compareVersions(parsed, maximum) <= 0)
|
|
166
|
+
return "in-row";
|
|
167
|
+
if (lowestMinimum === null || compareVersions(minimum, lowestMinimum) < 0)
|
|
168
|
+
lowestMinimum = minimum;
|
|
169
|
+
if (highestMaximum === null || compareVersions(maximum, highestMaximum) > 0)
|
|
170
|
+
highestMaximum = maximum;
|
|
171
|
+
}
|
|
172
|
+
if (lowestMinimum === null || highestMaximum === null)
|
|
173
|
+
return null;
|
|
174
|
+
if (compareVersions(parsed, lowestMinimum) < 0)
|
|
175
|
+
return "unknown-older";
|
|
176
|
+
if (compareVersions(parsed, highestMaximum) > 0)
|
|
177
|
+
return "unknown-newer";
|
|
178
|
+
return "below-range";
|
|
179
|
+
}
|
|
180
|
+
function rowsFor(rows, combination) {
|
|
181
|
+
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);
|
|
182
|
+
}
|
|
183
|
+
function admitManagedMutation(rows, combination) {
|
|
184
|
+
const keyed = rowsFor(rows, combination);
|
|
185
|
+
if (combination.providerVersion === null)
|
|
186
|
+
return {
|
|
187
|
+
state: "refused",
|
|
188
|
+
verdict: "no-row",
|
|
189
|
+
missing: "the exact provider version a row would record could not be established"
|
|
190
|
+
};
|
|
191
|
+
if (combination.harnessVersion === null)
|
|
192
|
+
return {
|
|
193
|
+
state: "refused",
|
|
194
|
+
verdict: "no-row",
|
|
195
|
+
missing: "the exact harness version a row would cover could not be established"
|
|
196
|
+
};
|
|
197
|
+
if (keyed.length === 0)
|
|
198
|
+
return {
|
|
199
|
+
state: "refused",
|
|
200
|
+
verdict: "no-row",
|
|
201
|
+
missing: `a reviewed config schema and fixture for ${combination.provider} on ${combination.harness} at ${combination.providerVersion} on ${combination.os}`
|
|
202
|
+
};
|
|
203
|
+
const versioned = keyed.filter((row3) => row3.providerVersion === combination.providerVersion);
|
|
204
|
+
if (versioned.length === 0) {
|
|
205
|
+
const fixture = keyed[0]?.fixture ?? "a provider fixture";
|
|
206
|
+
return {
|
|
207
|
+
state: "refused",
|
|
208
|
+
verdict: "no-row",
|
|
209
|
+
missing: `a provider fixture covering ${combination.provider} at ${combination.providerVersion} (the nearest row's fixture is ${JSON.stringify(fixture)})`
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const parsedHarness = parseSemanticVersion(combination.harnessVersion);
|
|
213
|
+
const row2 = versioned.find((candidate) => {
|
|
214
|
+
const minimum = parseSemanticVersion(candidate.harnessVersion.minimum);
|
|
215
|
+
const maximum = parseSemanticVersion(candidate.harnessVersion.maximum);
|
|
216
|
+
return minimum !== null && maximum !== null && parsedHarness !== null && compareVersions(parsedHarness, minimum) >= 0 && compareVersions(parsedHarness, maximum) <= 0;
|
|
217
|
+
});
|
|
218
|
+
if (row2 !== void 0)
|
|
219
|
+
return { state: "admitted", row: row2 };
|
|
220
|
+
const verdict = parsedHarness === null ? null : classifyRowHarnessVersion(versioned, combination.harnessVersion);
|
|
221
|
+
if (verdict === null || verdict === "in-row")
|
|
222
|
+
return {
|
|
223
|
+
state: "refused",
|
|
224
|
+
verdict: "no-row",
|
|
225
|
+
missing: `a reviewed config schema and fixture for ${combination.provider} on ${combination.harness}`
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
state: "refused",
|
|
229
|
+
verdict,
|
|
230
|
+
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))}`
|
|
231
|
+
};
|
|
112
232
|
}
|
|
113
233
|
|
|
114
234
|
// packages/core/dist/src/domain/compatibility.js
|
|
@@ -207,6 +327,7 @@ function harnessId(value) {
|
|
|
207
327
|
}
|
|
208
328
|
return value;
|
|
209
329
|
}
|
|
330
|
+
var UNATTRIBUTED_PROJECT_ID = "p_unattributed";
|
|
210
331
|
|
|
211
332
|
// packages/core/dist/src/domain/manifest.js
|
|
212
333
|
var MANIFEST_SCHEMA_VERSION = 1;
|
|
@@ -587,13 +708,14 @@ function resolveOwnership(input) {
|
|
|
587
708
|
});
|
|
588
709
|
continue;
|
|
589
710
|
}
|
|
590
|
-
if (!provider.
|
|
711
|
+
if (!provider.assignableHarnesses.has(harness.id)) {
|
|
712
|
+
const observed = input.observedVersions[provider.id] ?? null;
|
|
591
713
|
exclusions.push({
|
|
592
714
|
scope,
|
|
593
715
|
excluded: provider.id,
|
|
594
716
|
retained: null,
|
|
595
717
|
reason: [
|
|
596
|
-
`${provider.id} implements ${capability} but no installer state produces it
|
|
718
|
+
observed === null ? `${provider.id} implements ${capability} on ${harness.id} but no installer state this build could observe produces it there` : `${provider.id} ${observed} implements ${capability} on ${harness.id} but no installer state at that version produces it there`,
|
|
597
719
|
"RFC 0003: a capability the provider has but cannot be asked for is not an assignable capability",
|
|
598
720
|
"It is still detected, adopted, reconciled against the owner, and measured"
|
|
599
721
|
]
|
|
@@ -634,6 +756,7 @@ function resolveOwnership(input) {
|
|
|
634
756
|
}
|
|
635
757
|
}
|
|
636
758
|
}
|
|
759
|
+
narrowChannelOverlaps({ ownership, exclusions, conflicts, input });
|
|
637
760
|
const ownerByScope = new Map(ownership.map((entry) => [formatCapabilityScope(entry.scope), entry.owner]));
|
|
638
761
|
for (const exclusion of exclusions) {
|
|
639
762
|
if (exclusion.retained !== null)
|
|
@@ -644,6 +767,66 @@ function resolveOwnership(input) {
|
|
|
644
767
|
}
|
|
645
768
|
return { ownership, exclusions, conflicts, pipelineId: derivePipelineId(ownership) };
|
|
646
769
|
}
|
|
770
|
+
function narrowChannelOverlaps(context) {
|
|
771
|
+
const { ownership, exclusions, conflicts, input } = context;
|
|
772
|
+
const channels = /* @__PURE__ */ new Map();
|
|
773
|
+
for (const entry of ownership) {
|
|
774
|
+
if (entry.mode !== "exclusive")
|
|
775
|
+
continue;
|
|
776
|
+
const key = `${entry.scope.harness}/${entry.scope.toolFamily}/${entry.scope.capability}`;
|
|
777
|
+
const group = channels.get(key) ?? [];
|
|
778
|
+
group.push(entry);
|
|
779
|
+
channels.set(key, group);
|
|
780
|
+
}
|
|
781
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
782
|
+
for (const group of channels.values()) {
|
|
783
|
+
const owners = [...new Set(group.map((entry) => entry.owner))];
|
|
784
|
+
if (owners.length < 2)
|
|
785
|
+
continue;
|
|
786
|
+
const first = group[0];
|
|
787
|
+
if (first === void 0)
|
|
788
|
+
continue;
|
|
789
|
+
const { harness, toolFamily, capability } = first.scope;
|
|
790
|
+
const rule = findCompatibilityRule(input.rules, { providers: owners, harness, capability });
|
|
791
|
+
if (rule === null || rule.outcome !== "narrowed" || rule.retains === void 0) {
|
|
792
|
+
conflicts.push({
|
|
793
|
+
code: "exclusive-scope-contested",
|
|
794
|
+
scope: `${harness}/${toolFamily}/${capability}`,
|
|
795
|
+
claimants: [...owners].sort(),
|
|
796
|
+
detail: [
|
|
797
|
+
`${owners.join(" and ")} each own ${capability} on ${harness}/${toolFamily}, at different interception points`,
|
|
798
|
+
"The interception points differ but the payload does not, so the reduction would run twice",
|
|
799
|
+
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`
|
|
800
|
+
],
|
|
801
|
+
remediation: `Choose one owner for ${harness}/${toolFamily}/${capability} with \`profile: custom\``
|
|
802
|
+
});
|
|
803
|
+
for (const entry of group)
|
|
804
|
+
dropped.add(entry);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const retains = rule.retains;
|
|
808
|
+
for (const entry of group) {
|
|
809
|
+
if (entry.owner === retains)
|
|
810
|
+
continue;
|
|
811
|
+
dropped.add(entry);
|
|
812
|
+
exclusions.push({
|
|
813
|
+
scope: entry.scope,
|
|
814
|
+
excluded: entry.owner,
|
|
815
|
+
retained: retains,
|
|
816
|
+
reason: [
|
|
817
|
+
`rule ${rule.id} keeps ${capability} on ${harness}/${toolFamily} with ${retains}, at every interception point`,
|
|
818
|
+
`${entry.owner} is narrowed to the install its own flags produce rather than excluded whole`,
|
|
819
|
+
rule.rationale
|
|
820
|
+
]
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (dropped.size > 0) {
|
|
825
|
+
const kept = ownership.filter((entry) => !dropped.has(entry));
|
|
826
|
+
ownership.length = 0;
|
|
827
|
+
ownership.push(...kept);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
647
830
|
function resolveContested(context) {
|
|
648
831
|
const { scope, claims, capability, input, ownership, exclusions, conflicts } = context;
|
|
649
832
|
const claimants = claims.map((claim) => claim.provider);
|
|
@@ -719,6 +902,41 @@ function resolveContested(context) {
|
|
|
719
902
|
}
|
|
720
903
|
return;
|
|
721
904
|
}
|
|
905
|
+
if (rule.outcome === "narrowed") {
|
|
906
|
+
const retains = rule.retains;
|
|
907
|
+
if (retains === void 0) {
|
|
908
|
+
conflicts.push({
|
|
909
|
+
code: "compatibility-rule-malformed",
|
|
910
|
+
scope: formatCapabilityScope(scope),
|
|
911
|
+
claimants,
|
|
912
|
+
detail: [
|
|
913
|
+
`Rule ${rule.id} narrows a pair without naming which provider keeps the channel`,
|
|
914
|
+
"Choosing one here would make the resolver decide what the rule exists to record"
|
|
915
|
+
],
|
|
916
|
+
remediation: `Fix rule ${rule.id} so it names the provider that retains the capability`
|
|
917
|
+
});
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
for (const claimant of claimants) {
|
|
921
|
+
if (claimant === retains)
|
|
922
|
+
continue;
|
|
923
|
+
exclusions.push({
|
|
924
|
+
scope,
|
|
925
|
+
excluded: claimant,
|
|
926
|
+
retained: retains,
|
|
927
|
+
reason: [
|
|
928
|
+
`rule ${rule.id} keeps ${capability} on ${scope.harness} with ${retains}`,
|
|
929
|
+
`${claimant} is narrowed to the install its own flags produce rather than excluded whole`,
|
|
930
|
+
rule.rationale
|
|
931
|
+
]
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
const retainedClaim = claims.find((claim) => claim.provider === retains);
|
|
935
|
+
if (retainedClaim !== void 0) {
|
|
936
|
+
ownership.push({ scope, owner: retains, mode: retainedClaim.mode, order: 0 });
|
|
937
|
+
}
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
722
940
|
const winner = rule.providers.find((provider) => claimants.includes(provider)) ?? claimants[0];
|
|
723
941
|
for (const claimant of claimants) {
|
|
724
942
|
if (claimant === winner)
|
|
@@ -749,9 +967,10 @@ var RTK_HARNESSTRIM_SHELL_OVERLAP = {
|
|
|
749
967
|
// for different reasons. Listing them individually would imply a fourth harness is exempt.
|
|
750
968
|
harnesses: "*",
|
|
751
969
|
capabilities: ["shell.output.reduce"],
|
|
752
|
-
outcome: "
|
|
753
|
-
|
|
754
|
-
|
|
970
|
+
outcome: "narrowed",
|
|
971
|
+
retains: RTK,
|
|
972
|
+
testedVersions: { rtk: "0.44.0", harnesstrim: "0.1.0" },
|
|
973
|
+
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
974
|
fixtures: ["docs/rfcs/0003-capabilities-and-conflicts.md#the-table-is-an-intent"]
|
|
756
975
|
};
|
|
757
976
|
var COMPATIBILITY_RULES = [RTK_HARNESSTRIM_SHELL_OVERLAP];
|
|
@@ -820,21 +1039,21 @@ function validateStoredPlan(input) {
|
|
|
820
1039
|
detail: `the plan was computed for ${stored.projectRoot} and this invocation is in ${input.projectRoot}`
|
|
821
1040
|
});
|
|
822
1041
|
}
|
|
823
|
-
for (const [id,
|
|
1042
|
+
for (const [id, version3] of Object.entries(stored.versions.providers)) {
|
|
824
1043
|
const current = input.versions.providers[id];
|
|
825
|
-
if (current !==
|
|
1044
|
+
if (current !== version3) {
|
|
826
1045
|
rejections.push({
|
|
827
1046
|
reason: "plan-version-drift",
|
|
828
|
-
detail: `${id} was ${
|
|
1047
|
+
detail: `${id} was ${version3 ?? "absent"} when the plan was made and is now ${current ?? "absent"}`
|
|
829
1048
|
});
|
|
830
1049
|
}
|
|
831
1050
|
}
|
|
832
|
-
for (const [id,
|
|
1051
|
+
for (const [id, version3] of Object.entries(stored.versions.harnesses)) {
|
|
833
1052
|
const current = input.versions.harnesses[id];
|
|
834
|
-
if (current !==
|
|
1053
|
+
if (current !== version3) {
|
|
835
1054
|
rejections.push({
|
|
836
1055
|
reason: "plan-version-drift",
|
|
837
|
-
detail: `${id} was ${
|
|
1056
|
+
detail: `${id} was ${version3 ?? "absent"} when the plan was made and is now ${current ?? "absent"}`
|
|
838
1057
|
});
|
|
839
1058
|
}
|
|
840
1059
|
}
|
|
@@ -1121,7 +1340,7 @@ function resolveMetricsWindow(input) {
|
|
|
1121
1340
|
var INSTALL_COMMANDS = {
|
|
1122
1341
|
winget: {
|
|
1123
1342
|
executable: "winget",
|
|
1124
|
-
args: (packageName,
|
|
1343
|
+
args: (packageName, version3) => [
|
|
1125
1344
|
"install",
|
|
1126
1345
|
"--id",
|
|
1127
1346
|
packageName,
|
|
@@ -1131,16 +1350,16 @@ var INSTALL_COMMANDS = {
|
|
|
1131
1350
|
"--silent",
|
|
1132
1351
|
"--accept-package-agreements",
|
|
1133
1352
|
"--accept-source-agreements",
|
|
1134
|
-
...
|
|
1353
|
+
...version3 === null ? [] : ["--version", version3]
|
|
1135
1354
|
],
|
|
1136
1355
|
verified: true
|
|
1137
1356
|
},
|
|
1138
1357
|
cargo: {
|
|
1139
1358
|
executable: "cargo",
|
|
1140
|
-
args: (packageName,
|
|
1359
|
+
args: (packageName, version3) => [
|
|
1141
1360
|
"install",
|
|
1142
1361
|
packageName,
|
|
1143
|
-
...
|
|
1362
|
+
...version3 === null ? [] : ["--version", version3]
|
|
1144
1363
|
],
|
|
1145
1364
|
verified: false
|
|
1146
1365
|
}
|
|
@@ -1258,8 +1477,8 @@ async function queryAvailableVersion(input) {
|
|
|
1258
1477
|
}));
|
|
1259
1478
|
return { status: "failed", version: null, destination, diagnostics };
|
|
1260
1479
|
}
|
|
1261
|
-
const
|
|
1262
|
-
if (
|
|
1480
|
+
const version3 = recipe.parse(outcome2.stdout);
|
|
1481
|
+
if (version3 === null) {
|
|
1263
1482
|
diagnostics.push(diagnostic({
|
|
1264
1483
|
severity: "warning",
|
|
1265
1484
|
code: "version-query-unreadable",
|
|
@@ -1268,7 +1487,202 @@ async function queryAvailableVersion(input) {
|
|
|
1268
1487
|
}));
|
|
1269
1488
|
return { status: "unknown", version: null, destination, diagnostics };
|
|
1270
1489
|
}
|
|
1271
|
-
return { status: "found", version:
|
|
1490
|
+
return { status: "found", version: version3, destination, diagnostics };
|
|
1491
|
+
}
|
|
1492
|
+
var INVENTORY_COMMANDS = {
|
|
1493
|
+
/**
|
|
1494
|
+
* `winget list --id <id> --exact` prints the same dashes-anchored table as `show --versions`,
|
|
1495
|
+
* with the installed version as the last token of the row. The header is localized, which is
|
|
1496
|
+
* why the parse is anchored on the separator and the row tail rather than on a column title.
|
|
1497
|
+
*
|
|
1498
|
+
* A missing package makes winget exit non-zero with a localized "no package found" message,
|
|
1499
|
+
* which this build cannot tell from any other failure — so that case is `failed` at the caller,
|
|
1500
|
+
* never `absent`.
|
|
1501
|
+
*/
|
|
1502
|
+
winget: {
|
|
1503
|
+
executable: "winget",
|
|
1504
|
+
args: (packageName) => ["list", "--id", packageName, "--exact"],
|
|
1505
|
+
parse: (stdout, _packageName) => {
|
|
1506
|
+
const lines = stdout.split(/\r?\n/);
|
|
1507
|
+
const separator = lines.findIndex((line) => /^-{3,}\s*$/.test(line.trim()));
|
|
1508
|
+
if (separator < 0)
|
|
1509
|
+
return { status: "unknown", version: null };
|
|
1510
|
+
for (const line of lines.slice(separator + 1)) {
|
|
1511
|
+
const row2 = line.trim();
|
|
1512
|
+
if (row2 === "")
|
|
1513
|
+
continue;
|
|
1514
|
+
const candidate = row2.split(/\s+/).at(-1) ?? "";
|
|
1515
|
+
if (parseSemanticVersion(candidate) === null)
|
|
1516
|
+
return { status: "unknown", version: null };
|
|
1517
|
+
return { status: "captured", version: candidate };
|
|
1518
|
+
}
|
|
1519
|
+
return { status: "unknown", version: null };
|
|
1520
|
+
},
|
|
1521
|
+
verified: false
|
|
1522
|
+
},
|
|
1523
|
+
/**
|
|
1524
|
+
* `cargo install --list` prints one `crate v0.1.0:` line per installed crate, so absence is a
|
|
1525
|
+
* positive answer: the crate simply does not appear. This is the one inventory among the six
|
|
1526
|
+
* that can confirm absence.
|
|
1527
|
+
*/
|
|
1528
|
+
cargo: {
|
|
1529
|
+
executable: "cargo",
|
|
1530
|
+
args: () => ["install", "--list"],
|
|
1531
|
+
parse: (stdout, packageName) => {
|
|
1532
|
+
const pattern = new RegExp(`^\\s*${packageName}\\s+v(\\S+)\\s*:`, "m");
|
|
1533
|
+
const match = pattern.exec(stdout);
|
|
1534
|
+
if (match === null)
|
|
1535
|
+
return { status: "absent", version: null };
|
|
1536
|
+
const candidate = match[1] ?? "";
|
|
1537
|
+
if (parseSemanticVersion(candidate) === null)
|
|
1538
|
+
return { status: "unknown", version: null };
|
|
1539
|
+
return { status: "captured", version: candidate };
|
|
1540
|
+
},
|
|
1541
|
+
verified: false
|
|
1542
|
+
},
|
|
1543
|
+
npm: {
|
|
1544
|
+
executable: "npm",
|
|
1545
|
+
args: (packageName) => ["ls", "-g", packageName, "--depth=0"],
|
|
1546
|
+
parse: (stdout, packageName) => {
|
|
1547
|
+
const pattern = new RegExp(`(?:^|[^@\\w.-])${packageName}@(\\d[^\\s]*)`);
|
|
1548
|
+
const match = pattern.exec(stdout);
|
|
1549
|
+
const candidate = match?.[1] ?? null;
|
|
1550
|
+
if (candidate === null || parseSemanticVersion(candidate) === null) {
|
|
1551
|
+
return { status: "unknown", version: null };
|
|
1552
|
+
}
|
|
1553
|
+
return { status: "captured", version: candidate };
|
|
1554
|
+
},
|
|
1555
|
+
verified: false
|
|
1556
|
+
},
|
|
1557
|
+
homebrew: {
|
|
1558
|
+
executable: "brew",
|
|
1559
|
+
args: (packageName) => ["list", "--versions", packageName],
|
|
1560
|
+
parse: (stdout, packageName) => {
|
|
1561
|
+
const pattern = new RegExp(`^${packageName}\\s+(.+)$`);
|
|
1562
|
+
const match = pattern.exec(stdout.trim());
|
|
1563
|
+
const candidate = match?.[1]?.trim().split(/\s+/).at(-1) ?? null;
|
|
1564
|
+
if (candidate === null || parseSemanticVersion(candidate) === null) {
|
|
1565
|
+
return { status: "unknown", version: null };
|
|
1566
|
+
}
|
|
1567
|
+
return { status: "captured", version: candidate };
|
|
1568
|
+
},
|
|
1569
|
+
verified: false
|
|
1570
|
+
},
|
|
1571
|
+
uv: {
|
|
1572
|
+
executable: "uv",
|
|
1573
|
+
args: () => ["tool", "list"],
|
|
1574
|
+
parse: (stdout, packageName) => {
|
|
1575
|
+
const pattern = new RegExp(`^${packageName}\\s+v?(\\S+)`, "m");
|
|
1576
|
+
const match = pattern.exec(stdout);
|
|
1577
|
+
if (match === null)
|
|
1578
|
+
return { status: "absent", version: null };
|
|
1579
|
+
const candidate = match[1] ?? "";
|
|
1580
|
+
if (parseSemanticVersion(candidate) === null)
|
|
1581
|
+
return { status: "unknown", version: null };
|
|
1582
|
+
return { status: "captured", version: candidate };
|
|
1583
|
+
},
|
|
1584
|
+
verified: false
|
|
1585
|
+
},
|
|
1586
|
+
pipx: {
|
|
1587
|
+
executable: "pipx",
|
|
1588
|
+
args: () => ["list"],
|
|
1589
|
+
parse: (stdout, packageName) => {
|
|
1590
|
+
const pattern = new RegExp(`^\\s*${packageName}\\s+(\\S+)`, "m");
|
|
1591
|
+
const match = pattern.exec(stdout);
|
|
1592
|
+
if (match === null)
|
|
1593
|
+
return { status: "absent", version: null };
|
|
1594
|
+
const candidate = match[1] ?? "";
|
|
1595
|
+
if (parseSemanticVersion(candidate) === null)
|
|
1596
|
+
return { status: "unknown", version: null };
|
|
1597
|
+
return { status: "captured", version: candidate };
|
|
1598
|
+
},
|
|
1599
|
+
verified: false
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
function channelCanReportInventory(channelId) {
|
|
1603
|
+
return Object.hasOwn(INVENTORY_COMMANDS, channelId);
|
|
1604
|
+
}
|
|
1605
|
+
var DEFAULT_INVENTORY_TIMEOUT_MS = 6e4;
|
|
1606
|
+
async function queryPackageInventory(input) {
|
|
1607
|
+
const recipe = INVENTORY_COMMANDS[input.channel];
|
|
1608
|
+
if (recipe === void 0) {
|
|
1609
|
+
return {
|
|
1610
|
+
channel: input.channel,
|
|
1611
|
+
packageName: input.packageName,
|
|
1612
|
+
status: "unsupported",
|
|
1613
|
+
version: null,
|
|
1614
|
+
diagnostics: [
|
|
1615
|
+
diagnostic({
|
|
1616
|
+
severity: "warning",
|
|
1617
|
+
code: "inventory-query-unsupported",
|
|
1618
|
+
message: `This build cannot ask ${input.channel} what it has installed`,
|
|
1619
|
+
remediation: null
|
|
1620
|
+
})
|
|
1621
|
+
]
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
if (input.runner === null) {
|
|
1625
|
+
return {
|
|
1626
|
+
channel: input.channel,
|
|
1627
|
+
packageName: input.packageName,
|
|
1628
|
+
status: "failed",
|
|
1629
|
+
version: null,
|
|
1630
|
+
diagnostics: [
|
|
1631
|
+
diagnostic({
|
|
1632
|
+
severity: "error",
|
|
1633
|
+
code: "no-process-runner",
|
|
1634
|
+
message: "No process runner is available, so no inventory can be captured",
|
|
1635
|
+
remediation: null
|
|
1636
|
+
})
|
|
1637
|
+
]
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
const diagnostics = [];
|
|
1641
|
+
if (!recipe.verified) {
|
|
1642
|
+
diagnostics.push(diagnostic({
|
|
1643
|
+
severity: "warning",
|
|
1644
|
+
code: "inventory-query-unverified",
|
|
1645
|
+
message: `The ${input.channel} inventory query follows that tool's documented output but has not been observed working`,
|
|
1646
|
+
remediation: "Confirm the rollback receipt after any failure"
|
|
1647
|
+
}));
|
|
1648
|
+
}
|
|
1649
|
+
const outcome2 = await input.runner.run({
|
|
1650
|
+
executable: recipe.executable,
|
|
1651
|
+
args: recipe.args(input.packageName),
|
|
1652
|
+
cwd: input.cwd,
|
|
1653
|
+
timeoutMs: input.timeoutMs ?? DEFAULT_INVENTORY_TIMEOUT_MS
|
|
1654
|
+
});
|
|
1655
|
+
if (outcome2.failure !== null || outcome2.exitCode !== 0) {
|
|
1656
|
+
diagnostics.push(diagnostic({
|
|
1657
|
+
severity: "warning",
|
|
1658
|
+
code: "inventory-query-failed",
|
|
1659
|
+
message: outcome2.failure === null ? `${outcome2.displayCommand} exited with ${String(outcome2.exitCode)}` : `${recipe.executable} could not be run: ${outcome2.failure.reason}`,
|
|
1660
|
+
remediation: null
|
|
1661
|
+
}));
|
|
1662
|
+
return {
|
|
1663
|
+
channel: input.channel,
|
|
1664
|
+
packageName: input.packageName,
|
|
1665
|
+
status: "failed",
|
|
1666
|
+
version: null,
|
|
1667
|
+
diagnostics
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
const reading = recipe.parse(outcome2.stdout, input.packageName);
|
|
1671
|
+
if (reading.status === "unknown") {
|
|
1672
|
+
diagnostics.push(diagnostic({
|
|
1673
|
+
severity: "warning",
|
|
1674
|
+
code: "inventory-query-unreadable",
|
|
1675
|
+
message: `${recipe.executable} answered, but its output did not name an installed version in the form this build reads`,
|
|
1676
|
+
remediation: null
|
|
1677
|
+
}));
|
|
1678
|
+
}
|
|
1679
|
+
return {
|
|
1680
|
+
channel: input.channel,
|
|
1681
|
+
packageName: input.packageName,
|
|
1682
|
+
status: reading.status,
|
|
1683
|
+
version: reading.version,
|
|
1684
|
+
diagnostics
|
|
1685
|
+
};
|
|
1272
1686
|
}
|
|
1273
1687
|
var DEFAULT_INSTALL_TIMEOUT_MS = 6e5;
|
|
1274
1688
|
async function runPackageManagerInstall(input) {
|
|
@@ -1349,14 +1763,119 @@ async function runPackageManagerInstall(input) {
|
|
|
1349
1763
|
}));
|
|
1350
1764
|
return { status: "failed", diagnostics };
|
|
1351
1765
|
}
|
|
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
1766
|
return { status: "installed", diagnostics };
|
|
1359
1767
|
}
|
|
1768
|
+
async function restorePackageInventory(input) {
|
|
1769
|
+
const { capture } = input;
|
|
1770
|
+
if (capture.status === "absent") {
|
|
1771
|
+
return {
|
|
1772
|
+
restored: false,
|
|
1773
|
+
diagnostics: [
|
|
1774
|
+
diagnostic({
|
|
1775
|
+
severity: "info",
|
|
1776
|
+
code: "package-inventory-unrestored",
|
|
1777
|
+
message: `${capture.packageName} was not installed before the transaction; restoring that absence would require an uninstall command, so it stays installed`,
|
|
1778
|
+
remediation: null
|
|
1779
|
+
})
|
|
1780
|
+
]
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
if (capture.status !== "captured" || capture.version === null) {
|
|
1784
|
+
return {
|
|
1785
|
+
restored: false,
|
|
1786
|
+
diagnostics: [
|
|
1787
|
+
diagnostic({
|
|
1788
|
+
severity: "info",
|
|
1789
|
+
code: "package-inventory-unrestored",
|
|
1790
|
+
message: `${capture.packageName} was installed without a captured inventory, so it could not be restored`,
|
|
1791
|
+
remediation: null
|
|
1792
|
+
})
|
|
1793
|
+
]
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
if (input.runner === null) {
|
|
1797
|
+
return {
|
|
1798
|
+
restored: false,
|
|
1799
|
+
diagnostics: [
|
|
1800
|
+
diagnostic({
|
|
1801
|
+
severity: "info",
|
|
1802
|
+
code: "package-inventory-unrestored",
|
|
1803
|
+
message: `${capture.packageName} could not be restored: no process runner is available`,
|
|
1804
|
+
remediation: null
|
|
1805
|
+
})
|
|
1806
|
+
]
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
const restoreAction = {
|
|
1810
|
+
kind: "package-manager-install",
|
|
1811
|
+
id: `restore-${capture.packageName}`,
|
|
1812
|
+
riskClass: "delegated",
|
|
1813
|
+
requiresNetwork: true,
|
|
1814
|
+
requiresElevation: false,
|
|
1815
|
+
affectedPaths: [],
|
|
1816
|
+
affectedProcesses: [capture.channel],
|
|
1817
|
+
preconditions: [],
|
|
1818
|
+
postconditions: [],
|
|
1819
|
+
rollbackData: "none",
|
|
1820
|
+
explanation: `Restore ${capture.packageName} to ${capture.version} after a rollback`,
|
|
1821
|
+
packageManager: capture.channel,
|
|
1822
|
+
packageName: capture.packageName,
|
|
1823
|
+
version: capture.version
|
|
1824
|
+
};
|
|
1825
|
+
const result = await runPackageManagerInstall({
|
|
1826
|
+
action: restoreAction,
|
|
1827
|
+
runner: input.runner,
|
|
1828
|
+
cwd: input.cwd,
|
|
1829
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs }
|
|
1830
|
+
});
|
|
1831
|
+
if (result.status !== "installed") {
|
|
1832
|
+
return {
|
|
1833
|
+
restored: false,
|
|
1834
|
+
diagnostics: [
|
|
1835
|
+
...result.diagnostics,
|
|
1836
|
+
diagnostic({
|
|
1837
|
+
severity: "error",
|
|
1838
|
+
code: "package-restore-failed",
|
|
1839
|
+
message: `${capture.packageName} could not be restored to ${capture.version} through ${capture.channel}`,
|
|
1840
|
+
path: null,
|
|
1841
|
+
remediation: `Install ${capture.packageName} at ${capture.version} yourself, then run the command again`
|
|
1842
|
+
})
|
|
1843
|
+
]
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
const verified = await queryPackageInventory({
|
|
1847
|
+
channel: capture.channel,
|
|
1848
|
+
packageName: capture.packageName,
|
|
1849
|
+
runner: input.runner,
|
|
1850
|
+
cwd: input.cwd,
|
|
1851
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs }
|
|
1852
|
+
});
|
|
1853
|
+
if (verified.status !== "captured" || verified.version !== capture.version) {
|
|
1854
|
+
return {
|
|
1855
|
+
restored: false,
|
|
1856
|
+
diagnostics: [
|
|
1857
|
+
...verified.diagnostics,
|
|
1858
|
+
diagnostic({
|
|
1859
|
+
severity: "error",
|
|
1860
|
+
code: "package-restore-failed",
|
|
1861
|
+
message: `${capture.packageName} was reinstalled but the inventory does not report ${capture.version}`,
|
|
1862
|
+
remediation: `Confirm what ${capture.channel} has installed and correct it by hand`
|
|
1863
|
+
})
|
|
1864
|
+
]
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
restored: true,
|
|
1869
|
+
diagnostics: [
|
|
1870
|
+
diagnostic({
|
|
1871
|
+
severity: "info",
|
|
1872
|
+
code: "package-inventory-restored",
|
|
1873
|
+
message: `${capture.packageName} was restored to ${capture.version} through ${capture.channel} and the inventory was re-read`,
|
|
1874
|
+
remediation: null
|
|
1875
|
+
})
|
|
1876
|
+
]
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1360
1879
|
|
|
1361
1880
|
// packages/core/dist/src/state/json-merge.js
|
|
1362
1881
|
function isJsonObject(value) {
|
|
@@ -1776,7 +2295,8 @@ function outcome(action, status, parts = {}) {
|
|
|
1776
2295
|
status,
|
|
1777
2296
|
snapshots: parts.snapshots ?? [],
|
|
1778
2297
|
ownership: parts.ownership ?? [],
|
|
1779
|
-
diagnostics: parts.diagnostics ?? []
|
|
2298
|
+
diagnostics: parts.diagnostics ?? [],
|
|
2299
|
+
packageInventory: parts.packageInventory ?? null
|
|
1780
2300
|
};
|
|
1781
2301
|
}
|
|
1782
2302
|
function drift(action, path, message) {
|
|
@@ -2055,6 +2575,8 @@ async function scanTree(fs, path, entries = []) {
|
|
|
2055
2575
|
return entries;
|
|
2056
2576
|
}
|
|
2057
2577
|
function equalTreeEntry(left, right) {
|
|
2578
|
+
if (left === void 0 && right === void 0)
|
|
2579
|
+
return true;
|
|
2058
2580
|
return left !== void 0 && right !== void 0 && left.kind === right.kind && left.digest === right.digest;
|
|
2059
2581
|
}
|
|
2060
2582
|
async function applyDelegatedProviderInstall(action, context) {
|
|
@@ -2212,13 +2734,38 @@ async function applyAction(action, context) {
|
|
|
2212
2734
|
}
|
|
2213
2735
|
}
|
|
2214
2736
|
async function applyPackageManagerInstall(action, context) {
|
|
2737
|
+
const wantsInventory = action.rollbackData === "package-inventory" && !action.requiresElevation;
|
|
2738
|
+
const packageInventory = wantsInventory && context.runner !== void 0 ? await queryPackageInventory({
|
|
2739
|
+
channel: action.packageManager,
|
|
2740
|
+
packageName: action.packageName,
|
|
2741
|
+
runner: context.runner,
|
|
2742
|
+
cwd: context.cwd ?? ""
|
|
2743
|
+
}) : null;
|
|
2215
2744
|
const result = await runPackageManagerInstall({
|
|
2216
2745
|
action,
|
|
2217
2746
|
runner: context.runner ?? null,
|
|
2218
2747
|
cwd: context.cwd ?? ""
|
|
2219
2748
|
});
|
|
2220
2749
|
const status = result.status === "installed" ? "applied" : result.status === "refused" ? "refused" : "failed";
|
|
2221
|
-
|
|
2750
|
+
const diagnostics = [...result.diagnostics];
|
|
2751
|
+
if (status === "applied") {
|
|
2752
|
+
if (packageInventory?.status === "captured" && packageInventory.version !== null) {
|
|
2753
|
+
diagnostics.push(diagnostic({
|
|
2754
|
+
severity: "info",
|
|
2755
|
+
code: "install-inventory-captured",
|
|
2756
|
+
message: `${action.packageName} was installed through ${action.packageManager}; the prior state (${packageInventory.version}) was captured, so a rollback can restore it`,
|
|
2757
|
+
remediation: null
|
|
2758
|
+
}));
|
|
2759
|
+
} else {
|
|
2760
|
+
diagnostics.push(diagnostic({
|
|
2761
|
+
severity: "info",
|
|
2762
|
+
code: "install-not-reversible",
|
|
2763
|
+
message: `${action.packageName} was installed through ${action.packageManager}; a rollback restores files and will not uninstall it`,
|
|
2764
|
+
remediation: null
|
|
2765
|
+
}));
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return outcome(action, status, { diagnostics, packageInventory });
|
|
2222
2769
|
}
|
|
2223
2770
|
|
|
2224
2771
|
// packages/core/dist/src/state/journal.js
|
|
@@ -2599,10 +3146,10 @@ function parsePins(raw, path, diagnostics) {
|
|
|
2599
3146
|
if (typeof entry !== "object" || entry === null)
|
|
2600
3147
|
continue;
|
|
2601
3148
|
const provider = entry.provider;
|
|
2602
|
-
const
|
|
3149
|
+
const version3 = entry.version;
|
|
2603
3150
|
if (typeof provider !== "string" || provider === "")
|
|
2604
3151
|
continue;
|
|
2605
|
-
if (typeof
|
|
3152
|
+
if (typeof version3 !== "string" || parseSemanticVersion(version3) === null) {
|
|
2606
3153
|
diagnostics.push(diagnostic({
|
|
2607
3154
|
severity: "warning",
|
|
2608
3155
|
code: "pin-version-unparseable",
|
|
@@ -2611,7 +3158,7 @@ function parsePins(raw, path, diagnostics) {
|
|
|
2611
3158
|
}));
|
|
2612
3159
|
continue;
|
|
2613
3160
|
}
|
|
2614
|
-
pins.push({ provider, version:
|
|
3161
|
+
pins.push({ provider, version: version3 });
|
|
2615
3162
|
}
|
|
2616
3163
|
return pins;
|
|
2617
3164
|
}
|
|
@@ -2814,6 +3361,24 @@ async function verifyRestoration(fs, snapshots) {
|
|
|
2814
3361
|
}
|
|
2815
3362
|
return unrestored;
|
|
2816
3363
|
}
|
|
3364
|
+
async function restorePackageInventories(records, runner, cwd) {
|
|
3365
|
+
const diagnostics = [];
|
|
3366
|
+
for (const record of records) {
|
|
3367
|
+
if (record.kind !== "package-manager-install")
|
|
3368
|
+
continue;
|
|
3369
|
+
if (record.status !== "applied")
|
|
3370
|
+
continue;
|
|
3371
|
+
if (record.packageInventory === null || record.packageInventory === void 0)
|
|
3372
|
+
continue;
|
|
3373
|
+
const outcome2 = await restorePackageInventory({
|
|
3374
|
+
capture: record.packageInventory,
|
|
3375
|
+
runner,
|
|
3376
|
+
cwd
|
|
3377
|
+
});
|
|
3378
|
+
diagnostics.push(...outcome2.diagnostics);
|
|
3379
|
+
}
|
|
3380
|
+
return diagnostics;
|
|
3381
|
+
}
|
|
2817
3382
|
async function executeTransaction(request) {
|
|
2818
3383
|
const startedAt = request.now();
|
|
2819
3384
|
const entries = [];
|
|
@@ -2857,7 +3422,10 @@ async function executeTransaction(request) {
|
|
|
2857
3422
|
restoreError = error instanceof Error ? error.message : String(error);
|
|
2858
3423
|
}
|
|
2859
3424
|
const unrestored = await verifyRestoration(request.fs, snapshots);
|
|
2860
|
-
|
|
3425
|
+
const packageDiagnostics = await restorePackageInventories(applied, request.runner ?? null, request.projectRoot);
|
|
3426
|
+
diagnostics.push(...packageDiagnostics);
|
|
3427
|
+
const packageRestoreFailed = packageDiagnostics.some((entry) => entry.severity === "error");
|
|
3428
|
+
if (restoreError === null && unrestored.length === 0 && !packageRestoreFailed) {
|
|
2861
3429
|
diagnostics.push(diagnostic({
|
|
2862
3430
|
severity: "info",
|
|
2863
3431
|
code: "transaction-rolled-back",
|
|
@@ -2885,6 +3453,7 @@ async function executeTransaction(request) {
|
|
|
2885
3453
|
status: "failed",
|
|
2886
3454
|
snapshots: [],
|
|
2887
3455
|
ownership: [],
|
|
3456
|
+
packageInventory: null,
|
|
2888
3457
|
diagnostics: [
|
|
2889
3458
|
diagnostic({
|
|
2890
3459
|
severity: "error",
|
|
@@ -2902,7 +3471,8 @@ async function executeTransaction(request) {
|
|
|
2902
3471
|
status: result.status,
|
|
2903
3472
|
snapshots: result.snapshots,
|
|
2904
3473
|
ownership: result.ownership,
|
|
2905
|
-
diagnostics: result.diagnostics
|
|
3474
|
+
diagnostics: result.diagnostics,
|
|
3475
|
+
packageInventory: result.packageInventory
|
|
2906
3476
|
};
|
|
2907
3477
|
entries.push(entry);
|
|
2908
3478
|
applied.push(result);
|
|
@@ -2987,7 +3557,10 @@ async function rollbackTransaction(request) {
|
|
|
2987
3557
|
restoreError = error instanceof Error ? error.message : String(error);
|
|
2988
3558
|
}
|
|
2989
3559
|
const unrestored = await verifyRestoration(request.fs, snapshots);
|
|
2990
|
-
|
|
3560
|
+
const packageDiagnostics = await restorePackageInventories(journal.entries, request.runner ?? null, request.cwd ?? journal.projectRoot);
|
|
3561
|
+
diagnostics.push(...packageDiagnostics);
|
|
3562
|
+
const packageRestoreFailed = packageDiagnostics.some((entry) => entry.severity === "error");
|
|
3563
|
+
if (restoreError === null && unrestored.length === 0 && !packageRestoreFailed) {
|
|
2991
3564
|
journal.outcome = "rolled-back";
|
|
2992
3565
|
journal.ownership = [];
|
|
2993
3566
|
journal.finishedAt = request.now();
|
|
@@ -4493,8 +5066,8 @@ async function detectVersion2(context) {
|
|
|
4493
5066
|
};
|
|
4494
5067
|
}
|
|
4495
5068
|
const matched = VERSION_PATTERN.exec(outcome2.stdout);
|
|
4496
|
-
const
|
|
4497
|
-
if (
|
|
5069
|
+
const version3 = matched?.[1] ?? null;
|
|
5070
|
+
if (version3 === null) {
|
|
4498
5071
|
return {
|
|
4499
5072
|
version: null,
|
|
4500
5073
|
verdict: null,
|
|
@@ -4508,14 +5081,14 @@ async function detectVersion2(context) {
|
|
|
4508
5081
|
};
|
|
4509
5082
|
}
|
|
4510
5083
|
return {
|
|
4511
|
-
version:
|
|
4512
|
-
verdict: classifyVersion(
|
|
5084
|
+
version: version3,
|
|
5085
|
+
verdict: classifyVersion(version3, MANIFEST.testedVersions),
|
|
4513
5086
|
evidence: [
|
|
4514
5087
|
evidence({
|
|
4515
5088
|
kind: "version-output",
|
|
4516
5089
|
source: command.executable,
|
|
4517
5090
|
path: outcome2.executablePath,
|
|
4518
|
-
detail: `reported ${
|
|
5091
|
+
detail: `reported ${version3}`
|
|
4519
5092
|
})
|
|
4520
5093
|
]
|
|
4521
5094
|
};
|
|
@@ -4523,7 +5096,7 @@ async function detectVersion2(context) {
|
|
|
4523
5096
|
async function detect(context) {
|
|
4524
5097
|
const evidenceItems = [];
|
|
4525
5098
|
const warnings = [];
|
|
4526
|
-
const { version:
|
|
5099
|
+
const { version: version3, verdict, evidence: versionEvidence } = await detectVersion2(context);
|
|
4527
5100
|
evidenceItems.push(...versionEvidence);
|
|
4528
5101
|
const configs = await Promise.all(MANIFEST.configFiles.map((declaration) => resolveConfig(declaration, context)));
|
|
4529
5102
|
const present = configs.filter((config) => config.exists);
|
|
@@ -4546,7 +5119,7 @@ async function detect(context) {
|
|
|
4546
5119
|
remediation: "Repair the JSON syntax, then run the command again"
|
|
4547
5120
|
}));
|
|
4548
5121
|
}
|
|
4549
|
-
if (
|
|
5122
|
+
if (version3 === null && present.length > 0) {
|
|
4550
5123
|
warnings.push(diagnostic({
|
|
4551
5124
|
severity: "warning",
|
|
4552
5125
|
code: "harness-executable-missing",
|
|
@@ -4559,16 +5132,16 @@ async function detect(context) {
|
|
|
4559
5132
|
warnings.push(diagnostic({
|
|
4560
5133
|
severity: "warning",
|
|
4561
5134
|
code: "harness-version-unknown-newer",
|
|
4562
|
-
message: `Claude Code ${String(
|
|
5135
|
+
message: `Claude Code ${String(version3)} is newer than the versions whose configuration schema Token Harness has observed (up to ${String(MANIFEST.testedVersions.maximum)})`,
|
|
4563
5136
|
remediation: "Check the release notes for hook-schema changes before applying a plan"
|
|
4564
5137
|
}));
|
|
4565
5138
|
}
|
|
4566
|
-
const state = unparsed.length > 0 ? "broken" : configured.length > 0 &&
|
|
5139
|
+
const state = unparsed.length > 0 ? "broken" : configured.length > 0 && version3 !== null ? "configured" : version3 !== null || present.length > 0 ? "detected" : "absent";
|
|
4567
5140
|
const primary = present.find((config) => config.declaration.primary) ?? present[0] ?? null;
|
|
4568
5141
|
return {
|
|
4569
5142
|
harnessId: CLAUDE,
|
|
4570
5143
|
state,
|
|
4571
|
-
version:
|
|
5144
|
+
version: version3,
|
|
4572
5145
|
versionVerdict: verdict,
|
|
4573
5146
|
configPath: primary?.path ?? null,
|
|
4574
5147
|
declaredVerificationTier: MANIFEST.verificationTier,
|
|
@@ -4822,22 +5395,22 @@ async function detectVersion3(context) {
|
|
|
4822
5395
|
})
|
|
4823
5396
|
]
|
|
4824
5397
|
};
|
|
4825
|
-
const
|
|
5398
|
+
const version3 = VERSION_PATTERN2.exec(outcome2.stdout)?.[1] ?? null;
|
|
4826
5399
|
return {
|
|
4827
|
-
version:
|
|
4828
|
-
verdict:
|
|
5400
|
+
version: version3,
|
|
5401
|
+
verdict: version3 === null ? null : classifyVersion(version3, MANIFEST2.testedVersions),
|
|
4829
5402
|
evidence: [
|
|
4830
5403
|
evidence({
|
|
4831
5404
|
kind: "version-output",
|
|
4832
5405
|
source: "codex",
|
|
4833
5406
|
path: outcome2.executablePath,
|
|
4834
|
-
detail:
|
|
5407
|
+
detail: version3 === null ? "reported no recognisable semantic version" : `reported ${version3}`
|
|
4835
5408
|
})
|
|
4836
5409
|
]
|
|
4837
5410
|
};
|
|
4838
5411
|
}
|
|
4839
5412
|
async function detect2(context) {
|
|
4840
|
-
const { version:
|
|
5413
|
+
const { version: version3, verdict, evidence: versionEvidence } = await detectVersion3(context);
|
|
4841
5414
|
const configs = await Promise.all(MANIFEST2.configFiles.map((file) => resolveConfig2(file, context)));
|
|
4842
5415
|
const present = configs.filter((config) => config.exists);
|
|
4843
5416
|
const hooks = configs.find((config) => config.declaration.path.endsWith("hooks.json"));
|
|
@@ -4850,7 +5423,7 @@ async function detect2(context) {
|
|
|
4850
5423
|
path: hooks.path,
|
|
4851
5424
|
remediation: "Repair the JSON syntax, then run the command again"
|
|
4852
5425
|
}));
|
|
4853
|
-
if (
|
|
5426
|
+
if (version3 === null && present.length > 0)
|
|
4854
5427
|
warnings.push(diagnostic({
|
|
4855
5428
|
severity: "warning",
|
|
4856
5429
|
code: "harness-executable-missing",
|
|
@@ -4862,15 +5435,15 @@ async function detect2(context) {
|
|
|
4862
5435
|
warnings.push(diagnostic({
|
|
4863
5436
|
severity: "warning",
|
|
4864
5437
|
code: "harness-version-unknown-newer",
|
|
4865
|
-
message: `Codex ${String(
|
|
5438
|
+
message: `Codex ${String(version3)} is newer than the observed hook schema`,
|
|
4866
5439
|
remediation: "Check the release notes for hook-schema changes before applying a plan"
|
|
4867
5440
|
}));
|
|
4868
5441
|
const configured = (hooks?.configuredPoints.length ?? 0) > 0;
|
|
4869
5442
|
const primary = configs.find((config) => config.declaration.primary && config.exists) ?? present[0] ?? null;
|
|
4870
5443
|
return {
|
|
4871
5444
|
harnessId: CODEX,
|
|
4872
|
-
state: hooks?.exists && !hooks.parsed ? "broken" : configured &&
|
|
4873
|
-
version:
|
|
5445
|
+
state: hooks?.exists && !hooks.parsed ? "broken" : configured && version3 !== null ? "configured" : version3 !== null || present.length > 0 ? "detected" : "absent",
|
|
5446
|
+
version: version3,
|
|
4874
5447
|
versionVerdict: verdict,
|
|
4875
5448
|
configPath: primary?.path ?? null,
|
|
4876
5449
|
declaredVerificationTier: MANIFEST2.verificationTier,
|
|
@@ -4987,37 +5560,285 @@ async function verify2(context) {
|
|
|
4987
5560
|
}
|
|
4988
5561
|
var codexAdapter = { manifest: MANIFEST2, detect: detect2, inspect: inspect2, verify: verify2 };
|
|
4989
5562
|
|
|
4990
|
-
// packages/adapters/dist/src/harnesses/
|
|
4991
|
-
var
|
|
5563
|
+
// packages/adapters/dist/src/harnesses/hermes.js
|
|
5564
|
+
var HERMES = harnessId("hermes");
|
|
4992
5565
|
var VERSION_PATTERN3 = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/;
|
|
5566
|
+
var PLUGIN = "harnesstrim";
|
|
4993
5567
|
var MANIFEST3 = {
|
|
4994
5568
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
4995
|
-
id:
|
|
4996
|
-
displayName: "
|
|
4997
|
-
homepage: "https://
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5569
|
+
id: HERMES,
|
|
5570
|
+
displayName: "Hermes Agent",
|
|
5571
|
+
homepage: "https://hermes-agent.nousresearch.com/docs/",
|
|
5572
|
+
testedVersions: { minimum: "0.1.0", maximum: "0.1.0" },
|
|
5573
|
+
verificationTier: "config-only",
|
|
5574
|
+
versionCommand: { executable: "hermes", args: ["--version"] },
|
|
5575
|
+
interceptionPoints: [{ scopeId: "transform-tool-result", eventName: "transform_tool_result" }],
|
|
5576
|
+
configFiles: [
|
|
5577
|
+
{
|
|
5578
|
+
path: ".hermes/plugins/harnesstrim/plugin.yaml",
|
|
5579
|
+
scope: "user",
|
|
5580
|
+
parser: "markers",
|
|
5581
|
+
primary: true
|
|
5582
|
+
},
|
|
5583
|
+
{
|
|
5584
|
+
path: ".hermes/plugins/harnesstrim/config.json",
|
|
5585
|
+
scope: "user",
|
|
5586
|
+
parser: "json",
|
|
5587
|
+
primary: false
|
|
5588
|
+
}
|
|
5589
|
+
],
|
|
5590
|
+
toolFamilies: [
|
|
5591
|
+
{ id: "tool.result", platforms: ["windows", "macos", "linux"], executesShellCommands: false }
|
|
5592
|
+
],
|
|
5593
|
+
requiresEnablement: true,
|
|
5594
|
+
enablementNote: "The plugin must be enabled with `hermes plugins enable harnesstrim`.",
|
|
5595
|
+
receiptFamily: "provider-telemetry"
|
|
5596
|
+
};
|
|
5597
|
+
async function resolveConfig3(declaration, context) {
|
|
5598
|
+
const path = resolveConfigPath(declaration, context);
|
|
5599
|
+
const stat3 = await context.fs.stat(path);
|
|
5600
|
+
if (stat3 === null || stat3.kind !== "file") {
|
|
5601
|
+
return {
|
|
5602
|
+
declaration,
|
|
5603
|
+
path,
|
|
5604
|
+
exists: false,
|
|
5605
|
+
parsed: false,
|
|
5606
|
+
configuredPoints: [],
|
|
5607
|
+
matchers: [],
|
|
5608
|
+
commands: []
|
|
5609
|
+
};
|
|
5610
|
+
}
|
|
5611
|
+
const text = new TextDecoder().decode(await context.fs.readFile(path));
|
|
5612
|
+
const parsed = declaration.parser === "json" ? (() => {
|
|
5613
|
+
try {
|
|
5614
|
+
return JSON.parse(text);
|
|
5615
|
+
} catch {
|
|
5616
|
+
return null;
|
|
5617
|
+
}
|
|
5618
|
+
})() : text;
|
|
5619
|
+
const valid = parsed !== null;
|
|
5620
|
+
return {
|
|
5621
|
+
declaration,
|
|
5622
|
+
path,
|
|
5623
|
+
exists: true,
|
|
5624
|
+
parsed: valid,
|
|
5625
|
+
configuredPoints: valid && declaration.path.endsWith("plugin.yaml") ? ["transform-tool-result"] : [],
|
|
5626
|
+
matchers: valid && declaration.path.endsWith("plugin.yaml") ? ["tool.result"] : [],
|
|
5627
|
+
commands: valid && declaration.path.endsWith("plugin.yaml") ? ["hermes plugins enable harnesstrim"] : []
|
|
5628
|
+
};
|
|
5629
|
+
}
|
|
5630
|
+
async function version2(context) {
|
|
5631
|
+
const result = await context.runner.run({
|
|
5632
|
+
executable: "hermes",
|
|
5633
|
+
args: ["--version"],
|
|
5634
|
+
cwd: context.projectRoot,
|
|
5635
|
+
timeoutMs: 2e4
|
|
5636
|
+
});
|
|
5637
|
+
if (result.failure !== null)
|
|
5638
|
+
return {
|
|
5639
|
+
value: null,
|
|
5640
|
+
verdict: null,
|
|
5641
|
+
evidence: [
|
|
5642
|
+
evidence({
|
|
5643
|
+
kind: "absence",
|
|
5644
|
+
source: "hermes",
|
|
5645
|
+
detail: `not runnable: ${result.failure.reason}`
|
|
5646
|
+
})
|
|
5647
|
+
]
|
|
5648
|
+
};
|
|
5649
|
+
const value = VERSION_PATTERN3.exec(result.stdout)?.[1] ?? null;
|
|
5650
|
+
return {
|
|
5651
|
+
value,
|
|
5652
|
+
verdict: value === null ? null : classifyVersion(value, MANIFEST3.testedVersions),
|
|
5653
|
+
evidence: [
|
|
5654
|
+
evidence({
|
|
5655
|
+
kind: "version-output",
|
|
5656
|
+
source: "hermes",
|
|
5657
|
+
path: result.executablePath,
|
|
5658
|
+
detail: value ? `reported ${value}` : "reported no recognisable version"
|
|
5659
|
+
})
|
|
5660
|
+
]
|
|
5661
|
+
};
|
|
5662
|
+
}
|
|
5663
|
+
async function enabled(context) {
|
|
5664
|
+
const result = await context.runner.run({
|
|
5665
|
+
executable: "hermes",
|
|
5666
|
+
args: ["plugins", "list"],
|
|
5667
|
+
cwd: context.projectRoot,
|
|
5668
|
+
timeoutMs: 2e4
|
|
5669
|
+
});
|
|
5670
|
+
if (result.failure !== null || result.exitCode !== 0)
|
|
5671
|
+
return null;
|
|
5672
|
+
const line = result.stdout.split("\n").find((entry) => entry.toLowerCase().includes(PLUGIN));
|
|
5673
|
+
return line === void 0 ? false : /enabled/i.test(line);
|
|
5674
|
+
}
|
|
5675
|
+
async function detect3(context) {
|
|
5676
|
+
const observed = await version2(context);
|
|
5677
|
+
const configs = await Promise.all(MANIFEST3.configFiles.map((file) => resolveConfig3(file, context)));
|
|
5678
|
+
const plugin = configs.find((file) => file.declaration.path.endsWith("plugin.yaml"));
|
|
5679
|
+
const isEnabled = await enabled(context);
|
|
5680
|
+
const warnings = [];
|
|
5681
|
+
if (observed.verdict === "unknown-newer")
|
|
5682
|
+
warnings.push(diagnostic({
|
|
5683
|
+
severity: "warning",
|
|
5684
|
+
code: "harness-version-unknown-newer",
|
|
5685
|
+
message: `Hermes ${observed.value} is newer than the observed plugin contract`,
|
|
5686
|
+
remediation: "Review the transform_tool_result plugin contract before applying changes"
|
|
5687
|
+
}));
|
|
5688
|
+
if (plugin?.exists && isEnabled === false)
|
|
5689
|
+
warnings.push(diagnostic({
|
|
5690
|
+
severity: "warning",
|
|
5691
|
+
code: "plugin-disabled",
|
|
5692
|
+
message: "HarnessTrim is installed in Hermes but is not enabled",
|
|
5693
|
+
path: plugin.path,
|
|
5694
|
+
remediation: "Run `hermes plugins enable harnesstrim`"
|
|
5695
|
+
}));
|
|
5696
|
+
const present = plugin?.exists === true;
|
|
5697
|
+
return {
|
|
5698
|
+
harnessId: HERMES,
|
|
5699
|
+
state: present && isEnabled === true && observed.value !== null ? "configured" : present || observed.value !== null ? "detected" : "absent",
|
|
5700
|
+
version: observed.value,
|
|
5701
|
+
versionVerdict: observed.verdict,
|
|
5702
|
+
configPath: plugin?.path ?? null,
|
|
5703
|
+
declaredVerificationTier: MANIFEST3.verificationTier,
|
|
5704
|
+
evidence: [
|
|
5705
|
+
...observed.evidence,
|
|
5706
|
+
...plugin ? [
|
|
5707
|
+
evidence({
|
|
5708
|
+
kind: "config-entry",
|
|
5709
|
+
source: "Hermes plugin directory",
|
|
5710
|
+
path: plugin.path,
|
|
5711
|
+
detail: plugin.exists ? "HarnessTrim plugin manifest present" : "plugin manifest absent"
|
|
5712
|
+
})
|
|
5713
|
+
] : []
|
|
5714
|
+
],
|
|
5715
|
+
warnings
|
|
5716
|
+
};
|
|
5717
|
+
}
|
|
5718
|
+
async function inspect3(context) {
|
|
5719
|
+
const configs = await Promise.all(MANIFEST3.configFiles.map((file) => resolveConfig3(file, context)));
|
|
5720
|
+
return {
|
|
5721
|
+
harnessId: HERMES,
|
|
5722
|
+
configs,
|
|
5723
|
+
activeToolFamilies: familiesOnThisPlatform(MANIFEST3, context.facts),
|
|
5724
|
+
uncoveredToolFamilies: [],
|
|
5725
|
+
enabled: await enabled(context),
|
|
5726
|
+
diagnostics: [],
|
|
5727
|
+
summaries: configs.filter((config) => config.configuredPoints.length > 0).map((config) => ({
|
|
5728
|
+
harnessId: HERMES,
|
|
5729
|
+
configPath: config.path,
|
|
5730
|
+
scope: config.declaration.scope,
|
|
5731
|
+
interceptionPoints: config.configuredPoints,
|
|
5732
|
+
matchers: config.matchers,
|
|
5733
|
+
commands: config.commands
|
|
5734
|
+
}))
|
|
5735
|
+
};
|
|
5736
|
+
}
|
|
5737
|
+
async function verify3(context) {
|
|
5738
|
+
const detection = await detect3(context);
|
|
5739
|
+
const inspection = await inspect3(context);
|
|
5740
|
+
const plugin = inspection.configs.some((config) => config.configuredPoints.length > 0);
|
|
5741
|
+
const checks = [
|
|
5742
|
+
{
|
|
5743
|
+
id: "executable-resolves",
|
|
5744
|
+
status: detection.version === null ? "fail" : "pass",
|
|
5745
|
+
summary: detection.version === null ? "hermes could not be run" : `Hermes ${detection.version}`,
|
|
5746
|
+
achievedTier: detection.version === null ? null : "presence",
|
|
5747
|
+
evidence: detection.evidence.filter((item) => item.kind === "version-output"),
|
|
5748
|
+
remediation: detection.version === null ? "Install Hermes or add it to PATH" : null
|
|
5749
|
+
},
|
|
5750
|
+
{
|
|
5751
|
+
id: "plugin-installed",
|
|
5752
|
+
status: plugin ? "pass" : "fail",
|
|
5753
|
+
summary: plugin ? "HarnessTrim plugin manifest is installed" : "HarnessTrim plugin manifest is missing",
|
|
5754
|
+
achievedTier: plugin ? "config-only" : null,
|
|
5755
|
+
evidence: [],
|
|
5756
|
+
remediation: plugin ? null : "Run `harnesstrim install hermes --apply`"
|
|
5757
|
+
},
|
|
5758
|
+
{
|
|
5759
|
+
id: "plugin-enabled",
|
|
5760
|
+
status: inspection.enabled === true ? "pass" : inspection.enabled === false ? "fail" : "not-exercised",
|
|
5761
|
+
summary: inspection.enabled === true ? "HarnessTrim is enabled" : inspection.enabled === false ? "HarnessTrim is installed but disabled" : "Hermes plugin state could not be read",
|
|
5762
|
+
achievedTier: inspection.enabled === true ? "config-only" : null,
|
|
5763
|
+
evidence: [],
|
|
5764
|
+
remediation: inspection.enabled === false ? "Run `hermes plugins enable harnesstrim`" : null
|
|
5765
|
+
},
|
|
5766
|
+
{
|
|
5767
|
+
id: "canary-intercepted",
|
|
5768
|
+
status: "not-exercised",
|
|
5769
|
+
summary: "Hermes exposes no independent receipt; HarnessTrim telemetry is verified by the provider adapter",
|
|
5770
|
+
achievedTier: null,
|
|
5771
|
+
evidence: [],
|
|
5772
|
+
remediation: null
|
|
5773
|
+
}
|
|
5774
|
+
];
|
|
5775
|
+
return {
|
|
5776
|
+
harnessId: HERMES,
|
|
5777
|
+
declaredTier: MANIFEST3.verificationTier,
|
|
5778
|
+
achievedTier: checks.some((check) => check.achievedTier === "config-only") ? "config-only" : checks.some((check) => check.achievedTier === "presence") ? "presence" : null,
|
|
5779
|
+
checks
|
|
5780
|
+
};
|
|
5781
|
+
}
|
|
5782
|
+
var hermesAdapter = { manifest: MANIFEST3, detect: detect3, inspect: inspect3, verify: verify3 };
|
|
5783
|
+
|
|
5784
|
+
// packages/adapters/dist/src/harnesses/opencode.js
|
|
5785
|
+
var OPENCODE = harnessId("opencode");
|
|
5786
|
+
var VERSION_PATTERN4 = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/;
|
|
5787
|
+
var MANIFEST4 = {
|
|
5788
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
5789
|
+
id: OPENCODE,
|
|
5790
|
+
displayName: "OpenCode",
|
|
5791
|
+
homepage: "https://opencode.ai",
|
|
5792
|
+
/**
|
|
5793
|
+
* The version actually observed, not a round number.
|
|
5794
|
+
*
|
|
5795
|
+
* This read `1.0.0`–`1.0.0`, and the OpenCode on the machine this was reviewed against is
|
|
5796
|
+
* `1.18.9` — so every real user got `unknown-newer`, which `doctor` counts as a problem, and
|
|
5797
|
+
* every real machine exited 3. RFC 0006 §Exit codes forbids exactly that: "A supported
|
|
5798
|
+
* configuration must be able to exit 0. A declared limitation is not a problem, and reporting
|
|
5005
5799
|
* it as one is the fastest way to teach users to ignore the exit code."
|
|
5006
5800
|
*
|
|
5007
|
-
* A
|
|
5008
|
-
*
|
|
5801
|
+
* A range now, because spike 9.1 observed two more: the CLI at `1.18.11` and OpenCode Desktop at
|
|
5802
|
+
* `1.18.14`, both loading plugins from the same directories. The span is what was watched, not a
|
|
5803
|
+
* guess that every 1.18.x behaves alike — and the two ends differ in a way that matters, which is
|
|
5804
|
+
* exactly why both are named. An older 1.x reports `unknown-older`, which is a warning and not a
|
|
5009
5805
|
* problem, and a newer one reports `unknown-newer` — which is then a true statement.
|
|
5010
5806
|
*/
|
|
5011
|
-
testedVersions: { minimum: "1.18.9", maximum: "1.18.
|
|
5807
|
+
testedVersions: { minimum: "1.18.9", maximum: "1.18.14" },
|
|
5012
5808
|
// RFC 0007: a managed provider can emit a receipt, but an adopted generated wrapper
|
|
5013
5809
|
// has no externally observable receipt. The harness adapter alone therefore declares
|
|
5014
5810
|
// the achievable baseline, not a promise about a provider it does not own.
|
|
5015
5811
|
verificationTier: "config-only",
|
|
5016
5812
|
versionCommand: { executable: "opencode", args: ["--version"] },
|
|
5017
|
-
interceptionPoints: [
|
|
5813
|
+
interceptionPoints: [
|
|
5814
|
+
{ scopeId: "tool-execute-before", eventName: "tool.execute.before" },
|
|
5815
|
+
{ scopeId: "tool-execute-after", eventName: "tool.execute.after" }
|
|
5816
|
+
],
|
|
5817
|
+
/**
|
|
5818
|
+
* Two kinds of entry, because OpenCode has two ways to register a plugin and only one of them is
|
|
5819
|
+
* a document.
|
|
5820
|
+
*
|
|
5821
|
+
* The `opencode.jsonc` files carry a `plugin` string array. The directories are auto-loaded: any
|
|
5822
|
+
* module dropped in one is registered without being named anywhere, which is how both real
|
|
5823
|
+
* integrations install — `rtk init -g --opencode` writes `.config/opencode/plugins/rtk.ts` and
|
|
5824
|
+
* touches no JSON at all, and HarnessTrim's generated wrapper says in its own header not to
|
|
5825
|
+
* reference it from `opencode.json`. An adapter that read only the array would report every such
|
|
5826
|
+
* installation as absent, which is the state this file was in before spike 9.1.
|
|
5827
|
+
*
|
|
5828
|
+
* Both spellings are listed because both load. Spike 9.1 put a probe in each and one run fired
|
|
5829
|
+
* both; the upstream documentation names the plural, the binary carries both strings, and the two
|
|
5830
|
+
* shipped integrations disagree with each other about which to use.
|
|
5831
|
+
*
|
|
5832
|
+
* `markers` is the parser because there is nothing to parse: the presence of a file is the
|
|
5833
|
+
* configuration, and its contents are a plugin module rather than a document this build reads.
|
|
5834
|
+
*/
|
|
5018
5835
|
configFiles: [
|
|
5019
5836
|
{ path: ".config/opencode/opencode.jsonc", scope: "user", parser: "jsonc", primary: true },
|
|
5020
|
-
{ path: "opencode.jsonc", scope: "project", parser: "jsonc", primary: false }
|
|
5837
|
+
{ path: "opencode.jsonc", scope: "project", parser: "jsonc", primary: false },
|
|
5838
|
+
{ path: ".config/opencode/plugins", scope: "user", parser: "markers", primary: false },
|
|
5839
|
+
{ path: ".config/opencode/plugin", scope: "user", parser: "markers", primary: false },
|
|
5840
|
+
{ path: ".opencode/plugins", scope: "project", parser: "markers", primary: false },
|
|
5841
|
+
{ path: ".opencode/plugin", scope: "project", parser: "markers", primary: false }
|
|
5021
5842
|
],
|
|
5022
5843
|
toolFamilies: [
|
|
5023
5844
|
{ id: "tool.execute", platforms: ["windows", "macos", "linux"], executesShellCommands: true }
|
|
@@ -5034,7 +5855,38 @@ function pluginEntries(document2) {
|
|
|
5034
5855
|
return [];
|
|
5035
5856
|
return document2["plugin"].filter((item) => typeof item === "string");
|
|
5036
5857
|
}
|
|
5037
|
-
|
|
5858
|
+
var PLUGIN_MODULE_PATTERN = /\.(ts|js|mjs|cjs|mts|cts)$/i;
|
|
5859
|
+
var PLUGIN_POINTS = ["tool-execute-before", "tool-execute-after"];
|
|
5860
|
+
async function resolvePluginDirectory(declaration, context) {
|
|
5861
|
+
const path = resolveConfigPath(declaration, context);
|
|
5862
|
+
const stat3 = await context.fs.stat(path);
|
|
5863
|
+
if (stat3 === null || stat3.kind !== "directory") {
|
|
5864
|
+
return {
|
|
5865
|
+
declaration,
|
|
5866
|
+
path,
|
|
5867
|
+
exists: false,
|
|
5868
|
+
parsed: false,
|
|
5869
|
+
configuredPoints: [],
|
|
5870
|
+
matchers: [],
|
|
5871
|
+
commands: []
|
|
5872
|
+
};
|
|
5873
|
+
}
|
|
5874
|
+
const modules = (await context.fs.readDirectory(path)).filter((entry) => PLUGIN_MODULE_PATTERN.test(entry));
|
|
5875
|
+
return {
|
|
5876
|
+
declaration,
|
|
5877
|
+
path,
|
|
5878
|
+
exists: true,
|
|
5879
|
+
// A directory listing cannot fail to parse. `markers` declares that there is nothing to read.
|
|
5880
|
+
parsed: true,
|
|
5881
|
+
configuredPoints: modules.length > 0 ? [...PLUGIN_POINTS] : [],
|
|
5882
|
+
// The plugin API sees every tool result; this is not a shell matcher.
|
|
5883
|
+
matchers: modules.length > 0 ? ["tool.execute"] : [],
|
|
5884
|
+
commands: modules.map((entry) => context.fs.join(path, entry))
|
|
5885
|
+
};
|
|
5886
|
+
}
|
|
5887
|
+
async function resolveConfig4(declaration, context) {
|
|
5888
|
+
if (declaration.parser === "markers")
|
|
5889
|
+
return resolvePluginDirectory(declaration, context);
|
|
5038
5890
|
const path = resolveConfigPath(declaration, context);
|
|
5039
5891
|
const stat3 = await context.fs.stat(path);
|
|
5040
5892
|
if (stat3 === null || stat3.kind !== "file") {
|
|
@@ -5066,7 +5918,9 @@ async function resolveConfig3(declaration, context) {
|
|
|
5066
5918
|
path,
|
|
5067
5919
|
exists: true,
|
|
5068
5920
|
parsed: true,
|
|
5069
|
-
|
|
5921
|
+
// A `plugin` array entry is the same kind of thing as a module in a plugin directory: a
|
|
5922
|
+
// registration whose hooks are only known once it runs. Same claim, same reason.
|
|
5923
|
+
configuredPoints: plugins.length > 0 ? [...PLUGIN_POINTS] : [],
|
|
5070
5924
|
// The plugin API sees every tool result; this is not a shell matcher.
|
|
5071
5925
|
matchers: plugins.length > 0 ? ["tool.execute"] : [],
|
|
5072
5926
|
commands: plugins
|
|
@@ -5091,23 +5945,23 @@ async function detectVersion4(context) {
|
|
|
5091
5945
|
})
|
|
5092
5946
|
]
|
|
5093
5947
|
};
|
|
5094
|
-
const
|
|
5948
|
+
const version3 = VERSION_PATTERN4.exec(outcome2.stdout)?.[1] ?? null;
|
|
5095
5949
|
return {
|
|
5096
|
-
version:
|
|
5097
|
-
verdict:
|
|
5950
|
+
version: version3,
|
|
5951
|
+
verdict: version3 === null ? null : classifyVersion(version3, MANIFEST4.testedVersions),
|
|
5098
5952
|
evidence: [
|
|
5099
5953
|
evidence({
|
|
5100
5954
|
kind: "version-output",
|
|
5101
5955
|
source: "opencode",
|
|
5102
5956
|
path: outcome2.executablePath,
|
|
5103
|
-
detail:
|
|
5957
|
+
detail: version3 === null ? "reported no recognisable semantic version" : `reported ${version3}`
|
|
5104
5958
|
})
|
|
5105
5959
|
]
|
|
5106
5960
|
};
|
|
5107
5961
|
}
|
|
5108
|
-
async function
|
|
5109
|
-
const { version:
|
|
5110
|
-
const configs = await Promise.all(
|
|
5962
|
+
async function detect4(context) {
|
|
5963
|
+
const { version: version3, verdict, evidence: versionEvidence } = await detectVersion4(context);
|
|
5964
|
+
const configs = await Promise.all(MANIFEST4.configFiles.map((file) => resolveConfig4(file, context)));
|
|
5111
5965
|
const present = configs.filter((config) => config.exists);
|
|
5112
5966
|
const configured = present.filter((config) => config.configuredPoints.length > 0);
|
|
5113
5967
|
const warnings = [];
|
|
@@ -5123,17 +5977,17 @@ async function detect3(context) {
|
|
|
5123
5977
|
warnings.push(diagnostic({
|
|
5124
5978
|
severity: "warning",
|
|
5125
5979
|
code: "harness-version-unknown-newer",
|
|
5126
|
-
message: `OpenCode ${String(
|
|
5980
|
+
message: `OpenCode ${String(version3)} is newer than the observed configuration schema`,
|
|
5127
5981
|
remediation: "Check the release notes for plugin-schema changes before applying a plan"
|
|
5128
5982
|
}));
|
|
5129
5983
|
const primary = present.find((config) => config.declaration.primary) ?? present[0] ?? null;
|
|
5130
5984
|
return {
|
|
5131
5985
|
harnessId: OPENCODE,
|
|
5132
|
-
state: present.some((config) => !config.parsed) ? "broken" : configured.length > 0 &&
|
|
5133
|
-
version:
|
|
5986
|
+
state: present.some((config) => !config.parsed) ? "broken" : configured.length > 0 && version3 !== null ? "configured" : version3 !== null || present.length > 0 ? "detected" : "absent",
|
|
5987
|
+
version: version3,
|
|
5134
5988
|
versionVerdict: verdict,
|
|
5135
5989
|
configPath: primary?.path ?? null,
|
|
5136
|
-
declaredVerificationTier:
|
|
5990
|
+
declaredVerificationTier: MANIFEST4.verificationTier,
|
|
5137
5991
|
evidence: [
|
|
5138
5992
|
...versionEvidence,
|
|
5139
5993
|
...present.map((config) => evidence({
|
|
@@ -5146,18 +6000,224 @@ async function detect3(context) {
|
|
|
5146
6000
|
warnings
|
|
5147
6001
|
};
|
|
5148
6002
|
}
|
|
5149
|
-
async function
|
|
5150
|
-
const configs = await Promise.all(
|
|
5151
|
-
const activeToolFamilies = familiesOnThisPlatform(
|
|
6003
|
+
async function inspect4(context) {
|
|
6004
|
+
const configs = await Promise.all(MANIFEST4.configFiles.map((file) => resolveConfig4(file, context)));
|
|
6005
|
+
const activeToolFamilies = familiesOnThisPlatform(MANIFEST4, context.facts);
|
|
6006
|
+
return {
|
|
6007
|
+
harnessId: OPENCODE,
|
|
6008
|
+
configs,
|
|
6009
|
+
activeToolFamilies,
|
|
6010
|
+
uncoveredToolFamilies: [],
|
|
6011
|
+
enabled: null,
|
|
6012
|
+
diagnostics: [],
|
|
6013
|
+
summaries: configs.filter((config) => config.configuredPoints.length > 0).map((config) => ({
|
|
6014
|
+
harnessId: OPENCODE,
|
|
6015
|
+
configPath: config.path,
|
|
6016
|
+
scope: config.declaration.scope,
|
|
6017
|
+
interceptionPoints: config.configuredPoints,
|
|
6018
|
+
matchers: config.matchers,
|
|
6019
|
+
commands: config.commands
|
|
6020
|
+
}))
|
|
6021
|
+
};
|
|
6022
|
+
}
|
|
6023
|
+
async function verify4(context) {
|
|
6024
|
+
const detection = await detect4(context);
|
|
6025
|
+
const inspection = await inspect4(context);
|
|
6026
|
+
const configured = inspection.configs.some((config) => config.configuredPoints.length > 0);
|
|
6027
|
+
const readable = inspection.configs.filter((config) => config.exists).every((config) => config.parsed);
|
|
6028
|
+
const checks = [
|
|
6029
|
+
{
|
|
6030
|
+
id: "executable-resolves",
|
|
6031
|
+
status: detection.version === null ? "fail" : "pass",
|
|
6032
|
+
summary: detection.version === null ? "opencode could not be run" : `opencode ${detection.version} resolves`,
|
|
6033
|
+
achievedTier: detection.version === null ? null : "presence",
|
|
6034
|
+
evidence: detection.evidence.filter((item) => item.kind === "version-output"),
|
|
6035
|
+
remediation: detection.version === null ? "Install OpenCode or make it available on PATH" : null
|
|
6036
|
+
},
|
|
6037
|
+
{
|
|
6038
|
+
id: "config-readable",
|
|
6039
|
+
status: !readable ? "fail" : configured ? "pass" : "not-exercised",
|
|
6040
|
+
summary: !readable ? "OpenCode configuration is not valid JSONC" : configured ? "OpenCode plugin configuration is readable" : "No OpenCode plugin configuration is present",
|
|
6041
|
+
achievedTier: configured && readable ? "config-only" : null,
|
|
6042
|
+
evidence: [],
|
|
6043
|
+
remediation: !readable ? "Repair the JSONC syntax, then run verify again" : null
|
|
6044
|
+
},
|
|
6045
|
+
{
|
|
6046
|
+
id: "plugin-registered",
|
|
6047
|
+
status: configured ? "pass" : "not-exercised",
|
|
6048
|
+
summary: configured ? "A plugin entry is registered for tool.execute.after" : "No plugin entry is registered",
|
|
6049
|
+
achievedTier: configured ? "config-only" : null,
|
|
6050
|
+
evidence: [],
|
|
6051
|
+
remediation: configured ? null : "Configure the OpenCode plugin, or adopt an existing configuration"
|
|
6052
|
+
},
|
|
6053
|
+
{
|
|
6054
|
+
id: "canary-intercepted",
|
|
6055
|
+
status: "not-exercised",
|
|
6056
|
+
summary: "No observable receipt exists for an adopted generated plugin wrapper",
|
|
6057
|
+
achievedTier: null,
|
|
6058
|
+
evidence: [],
|
|
6059
|
+
remediation: null
|
|
6060
|
+
}
|
|
6061
|
+
];
|
|
6062
|
+
return {
|
|
6063
|
+
harnessId: OPENCODE,
|
|
6064
|
+
declaredTier: MANIFEST4.verificationTier,
|
|
6065
|
+
achievedTier: detection.version !== null && configured && readable ? "config-only" : detection.version !== null ? "presence" : null,
|
|
6066
|
+
checks
|
|
6067
|
+
};
|
|
6068
|
+
}
|
|
6069
|
+
var opencodeAdapter = { manifest: MANIFEST4, detect: detect4, inspect: inspect4, verify: verify4 };
|
|
6070
|
+
|
|
6071
|
+
// packages/adapters/dist/src/harnesses/pi.js
|
|
6072
|
+
var PI = harnessId("pi");
|
|
6073
|
+
var VERSION_PATTERN5 = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/;
|
|
6074
|
+
var MANIFEST5 = {
|
|
6075
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
6076
|
+
id: PI,
|
|
6077
|
+
displayName: "Pi",
|
|
6078
|
+
// The npm package declares no `homepage`; the repository it ships is the documented home.
|
|
6079
|
+
homepage: "https://github.com/earendil-works/pi",
|
|
6080
|
+
// Only the version actually observed — PLAN §15 item 31, "tested range of exactly the observed
|
|
6081
|
+
// version". `pi --version` on the machine this was written against printed `0.83.0`.
|
|
6082
|
+
testedVersions: { minimum: "0.83.0", maximum: "0.83.0" },
|
|
6083
|
+
// RFC 0007: the extension emits no receipt of its own, so nothing above `config-only` is
|
|
6084
|
+
// provable. The configuration is the extension module sitting in an auto-loaded directory.
|
|
6085
|
+
verificationTier: "config-only",
|
|
6086
|
+
versionCommand: { executable: "pi", args: ["--version"] },
|
|
6087
|
+
interceptionPoints: [{ scopeId: "tool-result", eventName: "tool_result" }],
|
|
6088
|
+
// `markers` because there is nothing to parse: Pi auto-loads any module in these directories,
|
|
6089
|
+
// and the presence of the HarnessTrim extension file is the whole configuration.
|
|
6090
|
+
configFiles: [
|
|
6091
|
+
{
|
|
6092
|
+
path: ".pi/agent/extensions/harnesstrim/index.ts",
|
|
6093
|
+
scope: "user",
|
|
6094
|
+
parser: "markers",
|
|
6095
|
+
primary: true
|
|
6096
|
+
},
|
|
6097
|
+
{
|
|
6098
|
+
path: ".pi/extensions/harnesstrim/harnesstrim.ts",
|
|
6099
|
+
scope: "project",
|
|
6100
|
+
parser: "markers",
|
|
6101
|
+
primary: false
|
|
6102
|
+
}
|
|
6103
|
+
],
|
|
6104
|
+
// The extension intercepts `tool_result` and reduces text chunks — a content seam, never a
|
|
6105
|
+
// shell. Mirrors the OpenCode plugin API claim.
|
|
6106
|
+
toolFamilies: [
|
|
6107
|
+
{ id: "tool.result", platforms: ["windows", "macos", "linux"], executesShellCommands: false }
|
|
6108
|
+
],
|
|
6109
|
+
// There is no separate enable state: the module being present in the directory is the load
|
|
6110
|
+
// decision, so the adapter reads presence and nothing else.
|
|
6111
|
+
requiresEnablement: false,
|
|
6112
|
+
enablementNote: null,
|
|
6113
|
+
receiptFamily: "none"
|
|
6114
|
+
};
|
|
6115
|
+
async function resolveConfig5(declaration, context) {
|
|
6116
|
+
const path = resolveConfigPath(declaration, context);
|
|
6117
|
+
const stat3 = await context.fs.stat(path);
|
|
6118
|
+
if (stat3 === null || stat3.kind !== "file") {
|
|
6119
|
+
return {
|
|
6120
|
+
declaration,
|
|
6121
|
+
path,
|
|
6122
|
+
exists: false,
|
|
6123
|
+
parsed: false,
|
|
6124
|
+
configuredPoints: [],
|
|
6125
|
+
matchers: [],
|
|
6126
|
+
commands: []
|
|
6127
|
+
};
|
|
6128
|
+
}
|
|
6129
|
+
const text = new TextDecoder().decode(await context.fs.readFile(path));
|
|
6130
|
+
const isHarnesstrim = /harnesstrim/i.test(text);
|
|
6131
|
+
return {
|
|
6132
|
+
declaration,
|
|
6133
|
+
path,
|
|
6134
|
+
exists: true,
|
|
6135
|
+
parsed: isHarnesstrim,
|
|
6136
|
+
configuredPoints: isHarnesstrim ? ["tool-result"] : [],
|
|
6137
|
+
matchers: isHarnesstrim ? ["tool.result"] : [],
|
|
6138
|
+
// There is no enable command: Pi loads the module from the directory directly.
|
|
6139
|
+
commands: []
|
|
6140
|
+
};
|
|
6141
|
+
}
|
|
6142
|
+
async function detectVersion5(context) {
|
|
6143
|
+
const result = await context.runner.run({
|
|
6144
|
+
executable: "pi",
|
|
6145
|
+
args: ["--version"],
|
|
6146
|
+
cwd: context.projectRoot,
|
|
6147
|
+
timeoutMs: 2e4
|
|
6148
|
+
});
|
|
6149
|
+
if (result.failure !== null)
|
|
6150
|
+
return {
|
|
6151
|
+
value: null,
|
|
6152
|
+
verdict: null,
|
|
6153
|
+
evidence: [
|
|
6154
|
+
evidence({
|
|
6155
|
+
kind: "absence",
|
|
6156
|
+
source: "pi",
|
|
6157
|
+
detail: `not runnable: ${result.failure.reason}`
|
|
6158
|
+
})
|
|
6159
|
+
]
|
|
6160
|
+
};
|
|
6161
|
+
const value = VERSION_PATTERN5.exec(result.stdout)?.[1] ?? null;
|
|
6162
|
+
return {
|
|
6163
|
+
value,
|
|
6164
|
+
verdict: value === null ? null : classifyVersion(value, MANIFEST5.testedVersions),
|
|
6165
|
+
evidence: [
|
|
6166
|
+
evidence({
|
|
6167
|
+
kind: "version-output",
|
|
6168
|
+
source: "pi",
|
|
6169
|
+
path: result.executablePath,
|
|
6170
|
+
detail: value ? `reported ${value}` : "reported no recognisable version"
|
|
6171
|
+
})
|
|
6172
|
+
]
|
|
6173
|
+
};
|
|
6174
|
+
}
|
|
6175
|
+
async function detect5(context) {
|
|
6176
|
+
const observed = await detectVersion5(context);
|
|
6177
|
+
const configs = await Promise.all(MANIFEST5.configFiles.map((file) => resolveConfig5(file, context)));
|
|
6178
|
+
const module = configs.find((file) => file.parsed);
|
|
6179
|
+
const present = module !== void 0;
|
|
6180
|
+
const warnings = [];
|
|
6181
|
+
if (observed.verdict === "unknown-newer")
|
|
6182
|
+
warnings.push(diagnostic({
|
|
6183
|
+
severity: "warning",
|
|
6184
|
+
code: "harness-version-unknown-newer",
|
|
6185
|
+
message: `Pi ${observed.value} is newer than the observed extension contract`,
|
|
6186
|
+
remediation: "Review the tool_result extension contract before applying changes"
|
|
6187
|
+
}));
|
|
6188
|
+
return {
|
|
6189
|
+
harnessId: PI,
|
|
6190
|
+
state: present && observed.value !== null ? "configured" : present || observed.value !== null ? "detected" : "absent",
|
|
6191
|
+
version: observed.value,
|
|
6192
|
+
versionVerdict: observed.verdict,
|
|
6193
|
+
configPath: module?.path ?? null,
|
|
6194
|
+
declaredVerificationTier: MANIFEST5.verificationTier,
|
|
6195
|
+
evidence: [
|
|
6196
|
+
...observed.evidence,
|
|
6197
|
+
...module ? [
|
|
6198
|
+
evidence({
|
|
6199
|
+
kind: "config-entry",
|
|
6200
|
+
source: "HarnessTrim Pi extension",
|
|
6201
|
+
path: module.path,
|
|
6202
|
+
detail: "extension module present in an auto-loaded directory"
|
|
6203
|
+
})
|
|
6204
|
+
] : []
|
|
6205
|
+
],
|
|
6206
|
+
warnings
|
|
6207
|
+
};
|
|
6208
|
+
}
|
|
6209
|
+
async function inspect5(context) {
|
|
6210
|
+
const configs = await Promise.all(MANIFEST5.configFiles.map((file) => resolveConfig5(file, context)));
|
|
5152
6211
|
return {
|
|
5153
|
-
harnessId:
|
|
6212
|
+
harnessId: PI,
|
|
5154
6213
|
configs,
|
|
5155
|
-
activeToolFamilies,
|
|
6214
|
+
activeToolFamilies: familiesOnThisPlatform(MANIFEST5, context.facts),
|
|
5156
6215
|
uncoveredToolFamilies: [],
|
|
6216
|
+
// No separate enablement exists, so nothing is reported as enabled or disabled.
|
|
5157
6217
|
enabled: null,
|
|
5158
6218
|
diagnostics: [],
|
|
5159
6219
|
summaries: configs.filter((config) => config.configuredPoints.length > 0).map((config) => ({
|
|
5160
|
-
harnessId:
|
|
6220
|
+
harnessId: PI,
|
|
5161
6221
|
configPath: config.path,
|
|
5162
6222
|
scope: config.declaration.scope,
|
|
5163
6223
|
interceptionPoints: config.configuredPoints,
|
|
@@ -5166,56 +6226,61 @@ async function inspect3(context) {
|
|
|
5166
6226
|
}))
|
|
5167
6227
|
};
|
|
5168
6228
|
}
|
|
5169
|
-
async function
|
|
5170
|
-
const detection = await
|
|
5171
|
-
const inspection = await
|
|
5172
|
-
const
|
|
5173
|
-
const readable = inspection.configs.filter((config) => config.exists).every((config) => config.parsed);
|
|
6229
|
+
async function verify5(context) {
|
|
6230
|
+
const detection = await detect5(context);
|
|
6231
|
+
const inspection = await inspect5(context);
|
|
6232
|
+
const module = inspection.configs.some((config) => config.configuredPoints.length > 0);
|
|
5174
6233
|
const checks = [
|
|
5175
6234
|
{
|
|
5176
6235
|
id: "executable-resolves",
|
|
5177
6236
|
status: detection.version === null ? "fail" : "pass",
|
|
5178
|
-
summary: detection.version === null ? "
|
|
6237
|
+
summary: detection.version === null ? "pi could not be run" : `Pi ${detection.version}`,
|
|
5179
6238
|
achievedTier: detection.version === null ? null : "presence",
|
|
5180
6239
|
evidence: detection.evidence.filter((item) => item.kind === "version-output"),
|
|
5181
|
-
remediation: detection.version === null ? "Install
|
|
6240
|
+
remediation: detection.version === null ? "Install Pi or add it to PATH" : null
|
|
5182
6241
|
},
|
|
5183
6242
|
{
|
|
5184
|
-
id: "
|
|
5185
|
-
status:
|
|
5186
|
-
summary:
|
|
5187
|
-
achievedTier:
|
|
6243
|
+
id: "module-installed",
|
|
6244
|
+
status: module ? "pass" : "fail",
|
|
6245
|
+
summary: module ? "HarnessTrim extension module is installed" : "No HarnessTrim extension module was found in the Pi extension directories",
|
|
6246
|
+
achievedTier: module ? "config-only" : null,
|
|
5188
6247
|
evidence: [],
|
|
5189
|
-
remediation:
|
|
6248
|
+
remediation: module ? null : "Run `harnesstrim install pi --apply`"
|
|
5190
6249
|
},
|
|
5191
6250
|
{
|
|
5192
|
-
id: "
|
|
5193
|
-
status:
|
|
5194
|
-
summary:
|
|
5195
|
-
achievedTier:
|
|
6251
|
+
id: "mode-unreadable",
|
|
6252
|
+
status: "info",
|
|
6253
|
+
summary: "The effective reduction mode (HARNESSTRIM_MODE) defaults to dryrun and is not observable from the extension alone",
|
|
6254
|
+
achievedTier: null,
|
|
5196
6255
|
evidence: [],
|
|
5197
|
-
remediation:
|
|
6256
|
+
remediation: null
|
|
5198
6257
|
},
|
|
5199
6258
|
{
|
|
5200
6259
|
id: "canary-intercepted",
|
|
5201
6260
|
status: "not-exercised",
|
|
5202
|
-
summary: "
|
|
6261
|
+
summary: "Pi exposes no independent receipt; HarnessTrim telemetry is verified by the provider adapter",
|
|
5203
6262
|
achievedTier: null,
|
|
5204
6263
|
evidence: [],
|
|
5205
6264
|
remediation: null
|
|
5206
6265
|
}
|
|
5207
6266
|
];
|
|
5208
6267
|
return {
|
|
5209
|
-
harnessId:
|
|
5210
|
-
declaredTier:
|
|
5211
|
-
achievedTier:
|
|
6268
|
+
harnessId: PI,
|
|
6269
|
+
declaredTier: MANIFEST5.verificationTier,
|
|
6270
|
+
achievedTier: checks.some((check) => check.achievedTier === "config-only") ? "config-only" : checks.some((check) => check.achievedTier === "presence") ? "presence" : null,
|
|
5212
6271
|
checks
|
|
5213
6272
|
};
|
|
5214
6273
|
}
|
|
5215
|
-
var
|
|
6274
|
+
var piAdapter = { manifest: MANIFEST5, detect: detect5, inspect: inspect5, verify: verify5 };
|
|
5216
6275
|
|
|
5217
6276
|
// packages/adapters/dist/src/harnesses/index.js
|
|
5218
|
-
var HARNESS_ADAPTERS = [
|
|
6277
|
+
var HARNESS_ADAPTERS = [
|
|
6278
|
+
claudeAdapter,
|
|
6279
|
+
codexAdapter,
|
|
6280
|
+
hermesAdapter,
|
|
6281
|
+
opencodeAdapter,
|
|
6282
|
+
piAdapter
|
|
6283
|
+
];
|
|
5219
6284
|
function listHarnessAdapters() {
|
|
5220
6285
|
return HARNESS_ADAPTERS;
|
|
5221
6286
|
}
|
|
@@ -5223,8 +6288,11 @@ function listHarnessAdapters() {
|
|
|
5223
6288
|
// packages/adapters/dist/src/providers/harnesstrim.js
|
|
5224
6289
|
var HARNESSTRIM2 = providerId("harnesstrim");
|
|
5225
6290
|
var CLAUDE2 = harnessId("claude");
|
|
6291
|
+
var HERMES2 = harnessId("hermes");
|
|
5226
6292
|
var CODEX2 = harnessId("codex");
|
|
5227
|
-
var
|
|
6293
|
+
var PI2 = harnessId("pi");
|
|
6294
|
+
var SKILLS_UPSTREAM = "0.0.7";
|
|
6295
|
+
var SKILL_ARTIFACT_DIGESTS = {
|
|
5228
6296
|
"compact-handoff/SKILL.md": "sha256:0efbf35581c559359e755b204778b64a289dab4d20dadc1cba3d5b5c995b5f01",
|
|
5229
6297
|
"debug-log-slim/SKILL.md": "sha256:17f5ccc34d29d7aba083444e0e4d87fd3d49916ebcb4e6544caf8c89f13f0045",
|
|
5230
6298
|
"delegate-bulk/SKILL.md": "sha256:3753de2ad18271c24832e4cda63115d353620515d5d80f40e93bc07d2a7257d7",
|
|
@@ -5233,9 +6301,18 @@ var CLAUDE_ARTIFACT_DIGESTS = {
|
|
|
5233
6301
|
"review-delta/SKILL.md": "sha256:4dfbf9d6ec08dff27b4759726536b43928370e288706fa01f5b498648e741388",
|
|
5234
6302
|
"scaffold-fast/SKILL.md": "sha256:1a18d52c4d335fbd3f74e2559bc20d6346193a40eef7b85149f51f44f697d182"
|
|
5235
6303
|
};
|
|
6304
|
+
var SKILL_ARTIFACTS = Object.keys(SKILL_ARTIFACT_DIGESTS);
|
|
5236
6305
|
var OPENCODE2 = harnessId("opencode");
|
|
6306
|
+
var MODE_CARRYING_HARNESSES = /* @__PURE__ */ new Set([
|
|
6307
|
+
OPENCODE2,
|
|
6308
|
+
HERMES2,
|
|
6309
|
+
harnessId("pi"),
|
|
6310
|
+
harnessId("omp")
|
|
6311
|
+
]);
|
|
5237
6312
|
var HOOK_COMMAND_PATTERN = /(^|[\\/\s"'])harnesstrim(\.cmd|\.exe)?([\s"']|$)/i;
|
|
5238
|
-
var
|
|
6313
|
+
var PLUGIN_MODULE_PATTERN2 = /(^|[\\/])harnesstrim\.(ts|js|mjs|cjs|mts|cts)$/i;
|
|
6314
|
+
var PI_EXTENSION_PATTERN = /(^|[\\/])\.pi[\\/](agent[\\/])?extensions[\\/]harnesstrim[\\/]/i;
|
|
6315
|
+
var MANIFEST6 = {
|
|
5239
6316
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
5240
6317
|
id: HARNESSTRIM2,
|
|
5241
6318
|
displayName: "HarnessTrim",
|
|
@@ -5311,36 +6388,264 @@ var MANIFEST4 = {
|
|
|
5311
6388
|
}
|
|
5312
6389
|
],
|
|
5313
6390
|
metrics: {
|
|
5314
|
-
// RFC 0005 §Importer degradation policy
|
|
5315
|
-
//
|
|
5316
|
-
//
|
|
6391
|
+
// RFC 0005 §Importer degradation policy. `native` since PLAN §15 item 43d: the `0.1.0`
|
|
6392
|
+
// telemetry lines carry a producer `eventId` and nullable token counts, so dedup uses the
|
|
6393
|
+
// native ID and a token count is a token count where one exists. Legacy schema 0 lines
|
|
6394
|
+
// still parse, stay `estimated-local`, and never merge with the native figures.
|
|
5317
6395
|
source: "jsonl",
|
|
5318
|
-
mode: "
|
|
5319
|
-
|
|
6396
|
+
mode: "native",
|
|
6397
|
+
// PLAN §15 item 25: Hermes has no adapter, tested range or tier, so the home-relative path that
|
|
6398
|
+
// names it is not read. Item 30 readmits it together with the adapter and a matrix row; the
|
|
6399
|
+
// registry assertion in `registries.test.ts` refuses a home-relative location that names a
|
|
6400
|
+
// harness the registry does not know.
|
|
6401
|
+
locations: [".harnesstrim/metrics.jsonl", ".hermes/harnesstrim-metrics.jsonl"]
|
|
5320
6402
|
},
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
6403
|
+
/**
|
|
6404
|
+
* One review per harness — PLAN §15 item 46b.
|
|
6405
|
+
*
|
|
6406
|
+
* Codex was observed at `0.1.0`: `harnesstrim install codex <dir> --apply --no-instructions`
|
|
6407
|
+
* writes the same seven skill artifacts as the Claude invocation, byte for byte — the digests
|
|
6408
|
+
* below are shared because the files are the same files — under `.codex/skills/` instead, and
|
|
6409
|
+
* skips `AGENTS.md` exactly as the Claude one skips `CLAUDE.md`. What differs is the boundary and
|
|
6410
|
+
* the protected paths, which is precisely why one review could not describe both.
|
|
6411
|
+
*
|
|
6412
|
+
* OpenCode is absent on purpose. Its installer writes `.opencode/plugin/harnesstrim.ts` and
|
|
6413
|
+
* `.opencode/package.json` *and runs an npm install*, so the containment boundary would hold a
|
|
6414
|
+
* `node_modules` tree: a snapshot question, not another entry in this map.
|
|
6415
|
+
*/
|
|
6416
|
+
delegatedInstallReviews: {
|
|
6417
|
+
claude: {
|
|
6418
|
+
upstreamVersion: SKILLS_UPSTREAM,
|
|
6419
|
+
reviewedWriteSet: SKILL_ARTIFACTS.map((path) => `.claude/skills/${path}`),
|
|
6420
|
+
containmentBoundary: [".claude", "CLAUDE.md"],
|
|
6421
|
+
upstreamUninstallAvailable: true
|
|
6422
|
+
},
|
|
6423
|
+
codex: {
|
|
6424
|
+
upstreamVersion: SKILLS_UPSTREAM,
|
|
6425
|
+
reviewedWriteSet: SKILL_ARTIFACTS.map((path) => `.codex/skills/${path}`),
|
|
6426
|
+
containmentBoundary: [".codex", "AGENTS.md"],
|
|
6427
|
+
upstreamUninstallAvailable: true
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
};
|
|
6431
|
+
var SKILLS_INSTALL = {
|
|
6432
|
+
claude: {
|
|
6433
|
+
directory: ".claude",
|
|
6434
|
+
instructions: "CLAUDE.md",
|
|
6435
|
+
hook: "settings.json",
|
|
6436
|
+
args: ["--no-hook", "--no-instructions"]
|
|
6437
|
+
},
|
|
6438
|
+
codex: {
|
|
6439
|
+
directory: ".codex",
|
|
6440
|
+
instructions: "AGENTS.md",
|
|
6441
|
+
hook: "hooks.json",
|
|
6442
|
+
args: ["--no-instructions"]
|
|
5328
6443
|
}
|
|
5329
6444
|
};
|
|
5330
6445
|
var TESTED_UPSTREAM = "0.0.7";
|
|
5331
6446
|
var TESTED_VERSIONS = { minimum: "0.0.5", maximum: TESTED_UPSTREAM };
|
|
5332
6447
|
function identifiesCommand(command) {
|
|
5333
|
-
return HOOK_COMMAND_PATTERN.test(command);
|
|
6448
|
+
return HOOK_COMMAND_PATTERN.test(command) || PLUGIN_MODULE_PATTERN2.test(command);
|
|
6449
|
+
}
|
|
6450
|
+
function isStringArray(value) {
|
|
6451
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
6452
|
+
}
|
|
6453
|
+
function parseHarnessCapabilities(value) {
|
|
6454
|
+
if (typeof value !== "object" || value === null)
|
|
6455
|
+
return null;
|
|
6456
|
+
const record = value;
|
|
6457
|
+
const surfaces = record["surfaces"];
|
|
6458
|
+
const writeSet = record["writeSet"];
|
|
6459
|
+
const narrowing = record["narrowing"];
|
|
6460
|
+
if (!isStringArray(surfaces) || !isStringArray(writeSet) || !Array.isArray(narrowing)) {
|
|
6461
|
+
return null;
|
|
6462
|
+
}
|
|
6463
|
+
const flags = [];
|
|
6464
|
+
for (const item of narrowing) {
|
|
6465
|
+
if (typeof item !== "object" || item === null)
|
|
6466
|
+
return null;
|
|
6467
|
+
const flag = item["flag"];
|
|
6468
|
+
const produces = item["produces"];
|
|
6469
|
+
if (typeof flag !== "string" || typeof produces !== "string")
|
|
6470
|
+
return null;
|
|
6471
|
+
flags.push({ flag, produces });
|
|
6472
|
+
}
|
|
6473
|
+
return {
|
|
6474
|
+
adapter: typeof record["adapter"] === "string" ? record["adapter"] : "",
|
|
6475
|
+
surfaces,
|
|
6476
|
+
writeSet,
|
|
6477
|
+
narrowing: flags
|
|
6478
|
+
};
|
|
6479
|
+
}
|
|
6480
|
+
function parseCapabilities(stdout) {
|
|
6481
|
+
let parsed;
|
|
6482
|
+
try {
|
|
6483
|
+
parsed = JSON.parse(stdout);
|
|
6484
|
+
} catch {
|
|
6485
|
+
return null;
|
|
6486
|
+
}
|
|
6487
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
6488
|
+
return null;
|
|
6489
|
+
const record = parsed;
|
|
6490
|
+
const version3 = record["version"];
|
|
6491
|
+
const harnesses = record["harnesses"];
|
|
6492
|
+
if (typeof version3 !== "string" || typeof harnesses !== "object" || harnesses === null) {
|
|
6493
|
+
return null;
|
|
6494
|
+
}
|
|
6495
|
+
const entries = {};
|
|
6496
|
+
for (const [harness, value] of Object.entries(harnesses)) {
|
|
6497
|
+
const entry = parseHarnessCapabilities(value);
|
|
6498
|
+
if (entry === null)
|
|
6499
|
+
return null;
|
|
6500
|
+
entries[harness] = entry;
|
|
6501
|
+
}
|
|
6502
|
+
return { version: version3, harnesses: entries };
|
|
6503
|
+
}
|
|
6504
|
+
async function probeCapabilities(context) {
|
|
6505
|
+
const outcome2 = await context.runner.run({
|
|
6506
|
+
executable: "harnesstrim",
|
|
6507
|
+
args: ["capabilities"],
|
|
6508
|
+
cwd: context.projectRoot,
|
|
6509
|
+
timeoutMs: 2e4
|
|
6510
|
+
});
|
|
6511
|
+
if (outcome2.failure !== null || outcome2.exitCode !== 0) {
|
|
6512
|
+
return {
|
|
6513
|
+
capabilities: null,
|
|
6514
|
+
evidence: [
|
|
6515
|
+
evidence({
|
|
6516
|
+
kind: "absence",
|
|
6517
|
+
source: "harnesstrim capabilities",
|
|
6518
|
+
detail: "did not answer with a machine-readable declaration"
|
|
6519
|
+
})
|
|
6520
|
+
]
|
|
6521
|
+
};
|
|
6522
|
+
}
|
|
6523
|
+
const capabilities = parseCapabilities(outcome2.stdout);
|
|
6524
|
+
if (capabilities === null) {
|
|
6525
|
+
return {
|
|
6526
|
+
capabilities: null,
|
|
6527
|
+
evidence: [
|
|
6528
|
+
evidence({
|
|
6529
|
+
kind: "version-output",
|
|
6530
|
+
source: "harnesstrim capabilities",
|
|
6531
|
+
path: outcome2.executablePath,
|
|
6532
|
+
detail: "answered, but not with a declaration this build can read"
|
|
6533
|
+
})
|
|
6534
|
+
]
|
|
6535
|
+
};
|
|
6536
|
+
}
|
|
6537
|
+
return {
|
|
6538
|
+
capabilities,
|
|
6539
|
+
evidence: [
|
|
6540
|
+
evidence({
|
|
6541
|
+
kind: "version-output",
|
|
6542
|
+
source: "harnesstrim capabilities",
|
|
6543
|
+
path: outcome2.executablePath,
|
|
6544
|
+
detail: `declared ${capabilities.version} for ${Object.keys(capabilities.harnesses).join(", ")}`
|
|
6545
|
+
})
|
|
6546
|
+
]
|
|
6547
|
+
};
|
|
6548
|
+
}
|
|
6549
|
+
var SURFACE_ANCHORS = {
|
|
6550
|
+
"post-tool-use": "PostToolUse",
|
|
6551
|
+
"tool-execute-after": "tool.execute.after"
|
|
6552
|
+
};
|
|
6553
|
+
function surfaceMatchesDeclaration(surface, declaration) {
|
|
6554
|
+
const pointAnchor = SURFACE_ANCHORS[declaration.interceptionPoint];
|
|
6555
|
+
if (pointAnchor !== void 0 && !surface.includes(pointAnchor))
|
|
6556
|
+
return false;
|
|
6557
|
+
if (declaration.toolFamily !== EVERY_TOOL_FAMILY && !surface.includes(declaration.toolFamily)) {
|
|
6558
|
+
return false;
|
|
6559
|
+
}
|
|
6560
|
+
return true;
|
|
6561
|
+
}
|
|
6562
|
+
function writeSetPath(entry) {
|
|
6563
|
+
return entry.replace(/\s*\([^)]*\)\s*$/, "").replaceAll("\\", "/");
|
|
6564
|
+
}
|
|
6565
|
+
function coveredBy(path, declared) {
|
|
6566
|
+
const base = declared.endsWith("/") ? declared : `${declared}/`;
|
|
6567
|
+
return path === declared || path.startsWith(base);
|
|
6568
|
+
}
|
|
6569
|
+
function driftWarning(message, remediation) {
|
|
6570
|
+
return diagnostic({
|
|
6571
|
+
severity: "warning",
|
|
6572
|
+
code: "provider-capabilities-drift",
|
|
6573
|
+
subject: "harnesstrim",
|
|
6574
|
+
message,
|
|
6575
|
+
remediation
|
|
6576
|
+
});
|
|
6577
|
+
}
|
|
6578
|
+
function formatSurface(surface) {
|
|
6579
|
+
return `${surface.toolFamily}/${surface.interceptionPoint}`;
|
|
6580
|
+
}
|
|
6581
|
+
function assignableOn(capabilities) {
|
|
6582
|
+
if (capabilities === null)
|
|
6583
|
+
return [];
|
|
6584
|
+
const reviews = MANIFEST6.delegatedInstallReviews ?? {};
|
|
6585
|
+
return Object.keys(reviews).filter((harness) => {
|
|
6586
|
+
const observed = capabilities.harnesses[harness];
|
|
6587
|
+
return observed !== void 0 && observed.narrowing.length > 0 && writeSetStillReviewed(capabilities, harnessId(harness));
|
|
6588
|
+
}).map((harness) => harnessId(harness));
|
|
6589
|
+
}
|
|
6590
|
+
function writeSetStillReviewed(capabilities, harness) {
|
|
6591
|
+
const review = MANIFEST6.delegatedInstallReviews?.[harness];
|
|
6592
|
+
if (review === void 0 || capabilities === null)
|
|
6593
|
+
return false;
|
|
6594
|
+
const observed = capabilities.harnesses[harness];
|
|
6595
|
+
if (observed === void 0)
|
|
6596
|
+
return false;
|
|
6597
|
+
const declared = observed.writeSet.map(writeSetPath);
|
|
6598
|
+
const boundary = review.containmentBoundary.map(writeSetPath);
|
|
6599
|
+
const everyDeclaredPathContained = declared.every((entry) => boundary.some((prefix) => coveredBy(entry, prefix)));
|
|
6600
|
+
const everyReviewedPathCovered = review.reviewedWriteSet.every((reviewed) => declared.some((entry) => coveredBy(reviewed, entry)));
|
|
6601
|
+
return everyDeclaredPathContained && everyReviewedPathCovered;
|
|
6602
|
+
}
|
|
6603
|
+
function compareCapabilities(manifest, capabilities) {
|
|
6604
|
+
const warnings = [];
|
|
6605
|
+
for (const declaration of manifest.capabilities) {
|
|
6606
|
+
for (const harness of declaration.harnesses) {
|
|
6607
|
+
const observed = capabilities.harnesses[harness];
|
|
6608
|
+
if (observed === void 0) {
|
|
6609
|
+
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`));
|
|
6610
|
+
continue;
|
|
6611
|
+
}
|
|
6612
|
+
const matched = declaration.surfaces.some((surface) => observed.surfaces.some((entry) => surfaceMatchesDeclaration(entry, surface)));
|
|
6613
|
+
if (!matched) {
|
|
6614
|
+
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`));
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
for (const [harness, review] of Object.entries(manifest.delegatedInstallReviews ?? {})) {
|
|
6619
|
+
const observed = capabilities.harnesses[harness];
|
|
6620
|
+
if (observed === void 0)
|
|
6621
|
+
continue;
|
|
6622
|
+
const declared = observed.writeSet.map(writeSetPath);
|
|
6623
|
+
for (const reviewed of review.reviewedWriteSet) {
|
|
6624
|
+
if (declared.some((entry) => coveredBy(reviewed, entry)))
|
|
6625
|
+
continue;
|
|
6626
|
+
warnings.push(driftWarning(`the reviewed ${harness} 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`));
|
|
6627
|
+
}
|
|
6628
|
+
const boundary = review.containmentBoundary.map(writeSetPath);
|
|
6629
|
+
for (const entry of declared) {
|
|
6630
|
+
if (boundary.some((prefix) => coveredBy(entry, prefix)))
|
|
6631
|
+
continue;
|
|
6632
|
+
warnings.push(driftWarning(`\`harnesstrim capabilities\` (${capabilities.version}) declares ${entry} for ${harness}, 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"));
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
return warnings;
|
|
5334
6636
|
}
|
|
5335
6637
|
function harnessesWiredToHarnessTrim(configs) {
|
|
5336
6638
|
const wired = /* @__PURE__ */ new Set();
|
|
5337
6639
|
for (const config of configs) {
|
|
5338
|
-
|
|
6640
|
+
const hermesPlugin = config.harnessId === HERMES2 && (config.configPath.toLowerCase().includes(".hermes/plugins/harnesstrim") || config.commands.some((command) => /hermes plugins enable harnesstrim/i.test(command)));
|
|
6641
|
+
const piExtension = config.harnessId === PI2 && PI_EXTENSION_PATTERN.test(config.configPath);
|
|
6642
|
+
if (piExtension || hermesPlugin || config.commands.some((command) => identifiesCommand(command))) {
|
|
5339
6643
|
wired.add(config.harnessId);
|
|
6644
|
+
}
|
|
5340
6645
|
}
|
|
5341
6646
|
return [...wired];
|
|
5342
6647
|
}
|
|
5343
|
-
var
|
|
6648
|
+
var VERSION_PATTERN6 = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/;
|
|
5344
6649
|
async function probeExecutable(context) {
|
|
5345
6650
|
const asked = await context.runner.run({
|
|
5346
6651
|
executable: "harnesstrim",
|
|
@@ -5363,17 +6668,17 @@ async function probeExecutable(context) {
|
|
|
5363
6668
|
};
|
|
5364
6669
|
}
|
|
5365
6670
|
if (asked.exitCode === 0) {
|
|
5366
|
-
const
|
|
6671
|
+
const version3 = VERSION_PATTERN6.exec(asked.stdout)?.[1] ?? null;
|
|
5367
6672
|
return {
|
|
5368
6673
|
installed: true,
|
|
5369
|
-
version:
|
|
6674
|
+
version: version3,
|
|
5370
6675
|
path: asked.executablePath,
|
|
5371
6676
|
evidence: [
|
|
5372
6677
|
evidence({
|
|
5373
6678
|
kind: "version-output",
|
|
5374
6679
|
source: "harnesstrim --version",
|
|
5375
6680
|
path: asked.executablePath,
|
|
5376
|
-
detail:
|
|
6681
|
+
detail: version3 === null ? "reported no recognisable version" : `reported ${version3}`
|
|
5377
6682
|
})
|
|
5378
6683
|
]
|
|
5379
6684
|
};
|
|
@@ -5418,11 +6723,15 @@ function metricsLocations(context) {
|
|
|
5418
6723
|
context.fs.join(context.paths.home, ".hermes", "harnesstrim-metrics.jsonl")
|
|
5419
6724
|
];
|
|
5420
6725
|
}
|
|
5421
|
-
async function
|
|
6726
|
+
async function detect6(context) {
|
|
5422
6727
|
const probe = await probeExecutable(context);
|
|
6728
|
+
const observed = probe.installed ? await probeCapabilities(context) : null;
|
|
5423
6729
|
const configured = harnessesWiredToHarnessTrim(context.harnessConfigs);
|
|
5424
6730
|
const warnings = [];
|
|
5425
|
-
const evidenceItems = [...probe.evidence];
|
|
6731
|
+
const evidenceItems = [...probe.evidence, ...observed?.evidence ?? []];
|
|
6732
|
+
if (observed !== null && observed.capabilities !== null) {
|
|
6733
|
+
warnings.push(...compareCapabilities(MANIFEST6, observed.capabilities));
|
|
6734
|
+
}
|
|
5426
6735
|
for (const harness of configured) {
|
|
5427
6736
|
evidenceItems.push(evidence({
|
|
5428
6737
|
kind: "config-entry",
|
|
@@ -5452,7 +6761,7 @@ async function detect4(context) {
|
|
|
5452
6761
|
// cannot answer.
|
|
5453
6762
|
versionVerdict: probe.version === null ? null : classifyVersion(probe.version, TESTED_VERSIONS),
|
|
5454
6763
|
configuredHarnesses: configured,
|
|
5455
|
-
unmanagedHarnessesConfigured: configured.filter((harness) => !
|
|
6764
|
+
unmanagedHarnessesConfigured: configured.filter((harness) => !MANIFEST6.harnesses.some((entry) => entry.harness === harness)),
|
|
5456
6765
|
// RFC 0002 §Providers may exceed the managed surface: HarnessTrim ships adapters for Hermes and
|
|
5457
6766
|
// Pi as well, which Token Harness does not manage, so a wired one is reported and left alone.
|
|
5458
6767
|
supportsUnmanagedHarnesses: true,
|
|
@@ -5460,6 +6769,9 @@ async function detect4(context) {
|
|
|
5460
6769
|
// circumstantial: PLAN §11 says Token Harness never installs it, so every installation it ever
|
|
5461
6770
|
// sees is the user's.
|
|
5462
6771
|
managedByTokenHarness: false,
|
|
6772
|
+
// PLAN §15 item 46, per harness. Decided by the build in front of us and by which harnesses
|
|
6773
|
+
// carry a reviewed write set: those are the two things that make the assignment producible.
|
|
6774
|
+
assignableHarnesses: assignableOn(observed?.capabilities ?? null),
|
|
5463
6775
|
evidence: evidenceItems,
|
|
5464
6776
|
warnings
|
|
5465
6777
|
};
|
|
@@ -5481,6 +6793,12 @@ function parseTrimEvent(line) {
|
|
|
5481
6793
|
if (typeof ts !== "string" || typeof harness !== "string" || typeof before !== "number" || typeof after !== "number") {
|
|
5482
6794
|
return null;
|
|
5483
6795
|
}
|
|
6796
|
+
const schemaVersion = typeof record["schemaVersion"] === "number" ? record["schemaVersion"] : 0;
|
|
6797
|
+
if (schemaVersion > 1)
|
|
6798
|
+
return null;
|
|
6799
|
+
const eventId = schemaVersion === 1 && typeof record["eventId"] === "string" && record["eventId"] !== "" ? record["eventId"] : null;
|
|
6800
|
+
if (schemaVersion === 1 && eventId === null)
|
|
6801
|
+
return null;
|
|
5484
6802
|
return {
|
|
5485
6803
|
ts,
|
|
5486
6804
|
harness,
|
|
@@ -5488,6 +6806,11 @@ function parseTrimEvent(line) {
|
|
|
5488
6806
|
reducer: typeof record["reducer"] === "string" ? record["reducer"] : null,
|
|
5489
6807
|
beforeChars: before,
|
|
5490
6808
|
afterChars: after,
|
|
6809
|
+
schemaVersion,
|
|
6810
|
+
eventId,
|
|
6811
|
+
changed: typeof record["changed"] === "boolean" ? record["changed"] : null,
|
|
6812
|
+
beforeTokens: typeof record["beforeTokens"] === "number" ? record["beforeTokens"] : null,
|
|
6813
|
+
afterTokens: typeof record["afterTokens"] === "number" ? record["afterTokens"] : null,
|
|
5491
6814
|
...typeof record["mode"] === "string" ? { mode: record["mode"] } : {}
|
|
5492
6815
|
};
|
|
5493
6816
|
}
|
|
@@ -5499,16 +6822,18 @@ function toEvent(event, sourceId, ordinal, line, context) {
|
|
|
5499
6822
|
const instant = new Date(event.ts);
|
|
5500
6823
|
if (Number.isNaN(instant.getTime()))
|
|
5501
6824
|
return null;
|
|
6825
|
+
const native = event.schemaVersion === 1;
|
|
5502
6826
|
const dryrun = event.mode === "dryrun";
|
|
5503
|
-
const eventId = synthesizeEventId(sourceId, ordinal, line);
|
|
6827
|
+
const eventId = native ? event.eventId : synthesizeEventId(sourceId, ordinal, line);
|
|
6828
|
+
const passThrough = native && event.changed === false;
|
|
6829
|
+
const hasTokens = native && event.beforeTokens !== null && event.afterTokens !== null;
|
|
6830
|
+
const hasUnprovenMode = native && MODE_CARRYING_HARNESSES.has(event.harness) && !hasTokens && !passThrough;
|
|
5504
6831
|
return {
|
|
5505
6832
|
schemaVersion: OPTIMIZATION_EVENT_SCHEMA_VERSION,
|
|
5506
6833
|
eventId,
|
|
5507
6834
|
timestamp: instant.toISOString(),
|
|
5508
6835
|
provider: { id: HARNESSTRIM2, version: null },
|
|
5509
6836
|
context: {
|
|
5510
|
-
// The project the metrics file belongs to, which for the default location is the project it
|
|
5511
|
-
// sits inside.
|
|
5512
6837
|
projectId: context.projectIdFor(context.projectRoot),
|
|
5513
6838
|
// Unlike RTK's database, a `TrimEvent` names its harness, so this is read rather than left
|
|
5514
6839
|
// unknown.
|
|
@@ -5518,31 +6843,40 @@ function toEvent(event, sourceId, ordinal, line, context) {
|
|
|
5518
6843
|
pipelineId: null,
|
|
5519
6844
|
pipelineOrder: null,
|
|
5520
6845
|
toolFamily: event.tool === "" ? null : event.tool,
|
|
5521
|
-
capability: event.harness === OPENCODE2 ? "tool.output.reduce" : "shell.output.reduce"
|
|
6846
|
+
capability: event.harness === OPENCODE2 || event.harness === HERMES2 || event.harness === PI2 ? "tool.output.reduce" : "shell.output.reduce"
|
|
5522
6847
|
},
|
|
5523
6848
|
measurement: {
|
|
5524
|
-
class: dryrun ? "counterfactual" : "estimated-local",
|
|
6849
|
+
class: dryrun || hasUnprovenMode ? "counterfactual" : hasTokens ? "exact-local" : "estimated-local",
|
|
5525
6850
|
beforeChars: event.beforeChars,
|
|
5526
6851
|
afterChars: event.afterChars,
|
|
5527
|
-
// RFC 0005: "never derived silently". `0.0.5` counts characters and nothing else
|
|
5528
|
-
|
|
5529
|
-
|
|
6852
|
+
// RFC 0005: "never derived silently". `0.0.5` counts characters and nothing else; the
|
|
6853
|
+
// `0.1.0` producer's counts are used as they are because a token count is a token count
|
|
6854
|
+
// where one exists.
|
|
6855
|
+
beforeTokens: hasTokens ? event.beforeTokens : null,
|
|
6856
|
+
afterTokens: hasTokens ? event.afterTokens : null,
|
|
5530
6857
|
tokenizer: null,
|
|
5531
6858
|
confidenceLow: null,
|
|
5532
6859
|
confidenceHigh: null
|
|
5533
6860
|
},
|
|
5534
6861
|
outcome: {
|
|
5535
|
-
// A dryrun leaves `output.output` untouched
|
|
5536
|
-
changed
|
|
5537
|
-
|
|
6862
|
+
// A dryrun leaves `output.output` untouched; a native line states whether its attempt
|
|
6863
|
+
// actually changed the output. A native line whose mode the envelope cannot carry is
|
|
6864
|
+
// deliberately not asserted as a change: the figure may describe a dryrun, and reporting
|
|
6865
|
+
// `changed: true` would claim the model received fewer bytes than it did.
|
|
6866
|
+
changed: dryrun || hasUnprovenMode ? false : native ? event.changed ?? true : event.afterChars !== event.beforeChars,
|
|
6867
|
+
bypassReason: dryrun ? "dryrun" : (
|
|
6868
|
+
// Same reason value as the diagnostic code for the residual ambiguity: nothing refutes a
|
|
6869
|
+
// dryrun, and nothing proves one, so the bypass is the envelope's own silence.
|
|
6870
|
+
hasUnprovenMode ? "mode-unresolved" : native ? passThrough ? "pass-through" : null : event.afterChars === event.beforeChars ? "no-reduction-applied" : null
|
|
6871
|
+
),
|
|
5538
6872
|
originalReference: null,
|
|
5539
6873
|
latencyMs: null,
|
|
5540
6874
|
errorCode: null
|
|
5541
6875
|
},
|
|
5542
|
-
source: { nativeEventId: null, importedAt: context.now() }
|
|
6876
|
+
source: { nativeEventId: native ? eventId : null, importedAt: context.now() }
|
|
5543
6877
|
};
|
|
5544
6878
|
}
|
|
5545
|
-
async function
|
|
6879
|
+
async function verify6(context) {
|
|
5546
6880
|
const checks = [];
|
|
5547
6881
|
const diagnostics = [];
|
|
5548
6882
|
const probe = await probeExecutable(context);
|
|
@@ -5590,7 +6924,7 @@ async function verify4(context) {
|
|
|
5590
6924
|
const agents = context.fs.join(context.projectRoot, "AGENTS.md");
|
|
5591
6925
|
if (await context.fs.stat(agents) !== null) {
|
|
5592
6926
|
const text = new TextDecoder().decode(await context.fs.readFile(agents));
|
|
5593
|
-
if (
|
|
6927
|
+
if (HOOK_COMMAND_PATTERN.test(text)) {
|
|
5594
6928
|
checks.push({
|
|
5595
6929
|
id: "instruction-path-present",
|
|
5596
6930
|
status: "info",
|
|
@@ -5626,6 +6960,8 @@ async function collectMetrics(context, store) {
|
|
|
5626
6960
|
let skipped = 0;
|
|
5627
6961
|
let lastCursor = null;
|
|
5628
6962
|
let readAny = false;
|
|
6963
|
+
let sawNative = false;
|
|
6964
|
+
let unprovableModeEvents = 0;
|
|
5629
6965
|
for (const path of metricsLocations(context)) {
|
|
5630
6966
|
const stat3 = await context.fs.stat(path);
|
|
5631
6967
|
if (stat3 === null)
|
|
@@ -5676,6 +7012,10 @@ async function collectMetrics(context, store) {
|
|
|
5676
7012
|
ordinal += 1;
|
|
5677
7013
|
continue;
|
|
5678
7014
|
}
|
|
7015
|
+
if (parsed.schemaVersion === 1)
|
|
7016
|
+
sawNative = true;
|
|
7017
|
+
if (event.outcome.bypassReason === "mode-unresolved")
|
|
7018
|
+
unprovableModeEvents += 1;
|
|
5679
7019
|
events.push(event);
|
|
5680
7020
|
lastLine = trimmed;
|
|
5681
7021
|
ordinal += 1;
|
|
@@ -5727,91 +7067,128 @@ async function collectMetrics(context, store) {
|
|
|
5727
7067
|
diagnostics
|
|
5728
7068
|
};
|
|
5729
7069
|
}
|
|
7070
|
+
if (unprovableModeEvents > 0) {
|
|
7071
|
+
diagnostics.push(diagnostic({
|
|
7072
|
+
severity: "info",
|
|
7073
|
+
code: "provider-metrics-mode-unresolved",
|
|
7074
|
+
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`,
|
|
7075
|
+
remediation: "Treat these figures as counterfactual; only OpenCode can later prove its mode from the configured plugin option"
|
|
7076
|
+
}));
|
|
7077
|
+
}
|
|
5730
7078
|
return {
|
|
5731
7079
|
providerId: HARNESSTRIM2,
|
|
5732
|
-
// RFC 0005 §Importer degradation policy
|
|
5733
|
-
//
|
|
5734
|
-
|
|
5735
|
-
|
|
7080
|
+
// RFC 0005 §Importer degradation policy, decided by what this run read: `native` when the
|
|
7081
|
+
// file carried schema 1 events with producer IDs, `legacy` when it only had the character
|
|
7082
|
+
// stream. Legacy inside a native run is not a problem — those lines are older files.
|
|
7083
|
+
//
|
|
7084
|
+
// A native stream whose mode-carrying harness lines cannot prove a realized reduction (no
|
|
7085
|
+
// tokens, no pass-through) is `native-with-residue`: the import is native, but a part of it
|
|
7086
|
+
// could not be classed as realized, and the count above qualifies exactly how much. A bare
|
|
7087
|
+
// `native` must keep meaning "everything was classed".
|
|
7088
|
+
mode: sawNative && unprovableModeEvents > 0 ? "native-with-residue" : sawNative ? "native" : "legacy",
|
|
7089
|
+
source: `harnesstrim TrimEvent JSONL (schema 0 and schema 1)`,
|
|
5736
7090
|
imported,
|
|
5737
7091
|
skipped,
|
|
5738
7092
|
cursor: lastCursor,
|
|
5739
7093
|
diagnostics
|
|
5740
7094
|
};
|
|
5741
7095
|
}
|
|
5742
|
-
|
|
5743
|
-
const
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
7096
|
+
function skillArtifacts(context, harness) {
|
|
7097
|
+
const install = SKILLS_INSTALL[harness];
|
|
7098
|
+
if (install === void 0)
|
|
7099
|
+
return [];
|
|
7100
|
+
const skills = context.fs.join(context.projectRoot, install.directory, "skills");
|
|
7101
|
+
return Object.entries(SKILL_ARTIFACT_DIGESTS).map(([path, digest]) => ({
|
|
7102
|
+
path: context.fs.join(skills, ...path.split("/")),
|
|
5747
7103
|
digest
|
|
5748
7104
|
}));
|
|
5749
|
-
|
|
7105
|
+
}
|
|
7106
|
+
async function plan(context, request) {
|
|
7107
|
+
const reviews = MANIFEST6.delegatedInstallReviews ?? {};
|
|
7108
|
+
const targets = request.harnesses.map((harness) => harness.id).filter((harness) => reviews[harness] !== void 0 && SKILLS_INSTALL[harness] !== void 0);
|
|
7109
|
+
if (targets.length === 0) {
|
|
5750
7110
|
return { providerId: HARNESSTRIM2, desiredState: request.desiredState, actions: [] };
|
|
5751
7111
|
}
|
|
5752
7112
|
if (request.desiredState === "absent") {
|
|
5753
|
-
const
|
|
7113
|
+
const actions2 = targets.flatMap((harness) => skillArtifacts(context, harness).map((artifact) => ({
|
|
5754
7114
|
kind: "remove-owned-change",
|
|
5755
|
-
id: `harnesstrim-
|
|
7115
|
+
id: `harnesstrim-${harness}-skill-remove-${digestText(artifact.path).slice(7, 15)}`,
|
|
5756
7116
|
riskClass: "reversible",
|
|
5757
7117
|
requiresNetwork: false,
|
|
5758
7118
|
requiresElevation: false,
|
|
5759
7119
|
affectedPaths: [artifact.path],
|
|
5760
7120
|
affectedProcesses: [],
|
|
5761
|
-
preconditions: [
|
|
7121
|
+
preconditions: [
|
|
7122
|
+
`the HarnessTrim skill still matches the reviewed ${SKILLS_UPSTREAM} artifact`
|
|
7123
|
+
],
|
|
5762
7124
|
postconditions: ["the owned HarnessTrim skill is absent"],
|
|
5763
7125
|
rollbackData: "file-snapshot",
|
|
5764
|
-
explanation: `Remove the owned HarnessTrim
|
|
7126
|
+
explanation: `Remove the owned HarnessTrim ${harness} skill ${context.fs.basename(context.fs.dirname(artifact.path))}`,
|
|
5765
7127
|
path: artifact.path,
|
|
5766
|
-
reverses: `harnesstrim-
|
|
5767
|
-
target: {
|
|
5768
|
-
|
|
5769
|
-
|
|
7128
|
+
reverses: `harnesstrim-${harness}-skills-${digestText(context.projectRoot).slice(7, 15)}`,
|
|
7129
|
+
target: {
|
|
7130
|
+
kind: "owned-file",
|
|
7131
|
+
path: artifact.path,
|
|
7132
|
+
digest: artifact.digest,
|
|
7133
|
+
mode: null
|
|
7134
|
+
}
|
|
7135
|
+
})));
|
|
7136
|
+
return { providerId: HARNESSTRIM2, desiredState: "absent", actions: actions2 };
|
|
5770
7137
|
}
|
|
5771
7138
|
const installed = await probeExecutable(context);
|
|
5772
|
-
if (installed.
|
|
7139
|
+
if (!installed.installed) {
|
|
5773
7140
|
return { providerId: HARNESSTRIM2, desiredState: "configured", actions: [] };
|
|
5774
7141
|
}
|
|
5775
|
-
const
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
7142
|
+
const observed = await probeCapabilities(context);
|
|
7143
|
+
const actions = [];
|
|
7144
|
+
for (const harness of targets) {
|
|
7145
|
+
const review = reviews[harness];
|
|
7146
|
+
const install = SKILLS_INSTALL[harness];
|
|
7147
|
+
if (review === void 0 || install === void 0)
|
|
7148
|
+
continue;
|
|
7149
|
+
const reviewed = observed.capabilities === null ? installed.version === review.upstreamVersion : writeSetStillReviewed(observed.capabilities, harnessId(harness));
|
|
7150
|
+
if (!reviewed)
|
|
7151
|
+
continue;
|
|
7152
|
+
const expectedArtifacts = skillArtifacts(context, harness);
|
|
7153
|
+
actions.push({
|
|
7154
|
+
kind: "delegated-provider-install",
|
|
7155
|
+
id: `harnesstrim-${harness}-skills-${digestText(context.projectRoot).slice(7, 15)}`,
|
|
7156
|
+
riskClass: "delegated",
|
|
7157
|
+
requiresNetwork: false,
|
|
7158
|
+
requiresElevation: false,
|
|
7159
|
+
affectedPaths: expectedArtifacts.map((artifact) => artifact.path),
|
|
7160
|
+
affectedProcesses: ["harnesstrim"],
|
|
7161
|
+
preconditions: [
|
|
7162
|
+
"harnesstrim is installed, on PATH, and declares the reviewed write set",
|
|
7163
|
+
`the reviewed installer writes only the declared ${harness} skill artifacts`
|
|
7164
|
+
],
|
|
7165
|
+
postconditions: [
|
|
7166
|
+
`HarnessTrim ${harness} skills match the reviewed ${review.upstreamVersion} artifacts`,
|
|
7167
|
+
"no HarnessTrim hook or reduce-pipe instruction was added"
|
|
7168
|
+
],
|
|
7169
|
+
rollbackData: "directory-snapshot",
|
|
7170
|
+
explanation: `Install HarnessTrim ${harness} skills without a hook or reduce-pipe instruction`,
|
|
7171
|
+
executable: "harnesstrim",
|
|
7172
|
+
args: ["install", harness, context.projectRoot, "--apply", ...install.args],
|
|
7173
|
+
containmentBoundary: review.containmentBoundary.map((path) => context.fs.join(context.projectRoot, path)),
|
|
7174
|
+
expectedArtifacts,
|
|
7175
|
+
// The two files each installer reports skipping. Named here because they must not appear.
|
|
7176
|
+
protectedPaths: [
|
|
7177
|
+
context.fs.join(context.projectRoot, install.directory, install.hook),
|
|
7178
|
+
context.fs.join(context.projectRoot, install.instructions)
|
|
7179
|
+
],
|
|
7180
|
+
rollbackStrategy: "restore-snapshot",
|
|
7181
|
+
snapshotSizeCapBytes: 1048576,
|
|
7182
|
+
upstreamUninstallAvailable: review.upstreamUninstallAvailable
|
|
7183
|
+
});
|
|
7184
|
+
}
|
|
7185
|
+
return { providerId: HARNESSTRIM2, desiredState: "configured", actions };
|
|
5809
7186
|
}
|
|
5810
7187
|
var harnesstrimAdapter = {
|
|
5811
|
-
manifest:
|
|
5812
|
-
detect:
|
|
7188
|
+
manifest: MANIFEST6,
|
|
7189
|
+
detect: detect6,
|
|
5813
7190
|
identifiesCommand,
|
|
5814
|
-
verify:
|
|
7191
|
+
verify: verify6,
|
|
5815
7192
|
collectMetrics,
|
|
5816
7193
|
plan,
|
|
5817
7194
|
plansWithoutOwnership: true
|
|
@@ -5819,6 +7196,7 @@ var harnesstrimAdapter = {
|
|
|
5819
7196
|
|
|
5820
7197
|
// packages/adapters/dist/src/providers/rtk-plan.js
|
|
5821
7198
|
var RTK_HOOK_COMMAND_PREFIX = "rtk hook";
|
|
7199
|
+
var HOOK_LIST_HARNESSES = /* @__PURE__ */ new Set(["claude"]);
|
|
5822
7200
|
function hookListPointer(eventName) {
|
|
5823
7201
|
return `hooks.${eventName.replace(/\./g, "\\.")}`;
|
|
5824
7202
|
}
|
|
@@ -5834,6 +7212,8 @@ function planTargets(context, request) {
|
|
|
5834
7212
|
const harness = request.harnesses.find((entry) => entry.id === owned.scope.harness);
|
|
5835
7213
|
if (harness === void 0)
|
|
5836
7214
|
continue;
|
|
7215
|
+
if (!HOOK_LIST_HARNESSES.has(harness.id))
|
|
7216
|
+
continue;
|
|
5837
7217
|
const point = harness.interceptionPoints.find((entry) => entry.scopeId === owned.scope.interceptionPoint);
|
|
5838
7218
|
if (point === void 0)
|
|
5839
7219
|
continue;
|
|
@@ -5966,11 +7346,11 @@ function installAction(context, manifest) {
|
|
|
5966
7346
|
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
7347
|
],
|
|
5968
7348
|
postconditions: ["`rtk --version` reports a version inside the tested range"],
|
|
5969
|
-
//
|
|
5970
|
-
//
|
|
5971
|
-
//
|
|
5972
|
-
//
|
|
5973
|
-
rollbackData: "none",
|
|
7349
|
+
// The declared value the executor now implements (RFC 0009 §Initial delivery order item 1):
|
|
7350
|
+
// the executor captures whatever the channel has installed before the install runs, and a
|
|
7351
|
+
// rollback restores that captured version through the same channel. `none` is left only for
|
|
7352
|
+
// channels this build cannot ask about — there the executor says the package stayed instead.
|
|
7353
|
+
rollbackData: channelCanReportInventory(channel.id) ? "package-inventory" : "none",
|
|
5974
7354
|
explanation: `Install RTK through ${channel.id}`,
|
|
5975
7355
|
packageManager: channel.id,
|
|
5976
7356
|
// The channel's own name for the package, because they differ: `rtk-ai.rtk` on winget and
|
|
@@ -6009,7 +7389,8 @@ function buildRtkPlan(input) {
|
|
|
6009
7389
|
// packages/adapters/dist/src/providers/rtk.js
|
|
6010
7390
|
var RTK2 = providerId("rtk");
|
|
6011
7391
|
var CLAUDE3 = harnessId("claude");
|
|
6012
|
-
var
|
|
7392
|
+
var OPENCODE3 = harnessId("opencode");
|
|
7393
|
+
var MANIFEST7 = {
|
|
6013
7394
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
6014
7395
|
id: RTK2,
|
|
6015
7396
|
displayName: "RTK",
|
|
@@ -6047,10 +7428,53 @@ var MANIFEST5 = {
|
|
|
6047
7428
|
sourceReference: "docs/spikes/2.5-live-verification-log.md",
|
|
6048
7429
|
upstreamVersion: "0.42.0"
|
|
6049
7430
|
}
|
|
7431
|
+
},
|
|
7432
|
+
/**
|
|
7433
|
+
* OpenCode, from spike 9.1.
|
|
7434
|
+
*
|
|
7435
|
+
* Separate entries rather than adding `OPENCODE` to the two above, because the surface is not
|
|
7436
|
+
* the same one: RTK reaches Claude Code through a `PreToolUse` hook matching `Bash`, and
|
|
7437
|
+
* OpenCode through a plugin on `tool.execute.before`. A capability entry names one surface
|
|
7438
|
+
* list, and merging these would claim that RTK serves `Bash/pre-tool-use` on OpenCode — a
|
|
7439
|
+
* point OpenCode does not have.
|
|
7440
|
+
*
|
|
7441
|
+
* `shell.command.rewrite` and `shell.output.reduce` again arrive together, for the reason they
|
|
7442
|
+
* do on Claude Code: the plugin calls `rtk rewrite`, and the rewritten command is what filters
|
|
7443
|
+
* its own output. One interception point, two capabilities.
|
|
7444
|
+
*/
|
|
7445
|
+
{
|
|
7446
|
+
capability: "shell.command.rewrite",
|
|
7447
|
+
mode: "exclusive",
|
|
7448
|
+
harnesses: [OPENCODE3],
|
|
7449
|
+
// `tool.execute` is the family the harness manifest declares as shell-executing. The plugin
|
|
7450
|
+
// narrows to `tool === "bash" || tool === "shell"` inside that family, which is every shell
|
|
7451
|
+
// route OpenCode exposes — unlike Claude Code on Windows, there is no second one to miss.
|
|
7452
|
+
surfaces: [{ toolFamily: "tool.execute", interceptionPoint: "tool-execute-before" }],
|
|
7453
|
+
evidence: {
|
|
7454
|
+
sourceReference: "docs/spikes/9.1-rtk-opencode-observation-log.md",
|
|
7455
|
+
upstreamVersion: "0.44.0"
|
|
7456
|
+
}
|
|
7457
|
+
},
|
|
7458
|
+
{
|
|
7459
|
+
capability: "shell.output.reduce",
|
|
7460
|
+
mode: "exclusive",
|
|
7461
|
+
harnesses: [OPENCODE3],
|
|
7462
|
+
surfaces: [{ toolFamily: "tool.execute", interceptionPoint: "tool-execute-before" }],
|
|
7463
|
+
evidence: {
|
|
7464
|
+
sourceReference: "docs/spikes/9.1-rtk-opencode-observation-log.md",
|
|
7465
|
+
upstreamVersion: "0.44.0"
|
|
7466
|
+
}
|
|
6050
7467
|
}
|
|
6051
7468
|
],
|
|
6052
7469
|
platforms: [
|
|
6053
|
-
{
|
|
7470
|
+
{
|
|
7471
|
+
os: "windows",
|
|
7472
|
+
wsl: false,
|
|
7473
|
+
supported: true,
|
|
7474
|
+
// Spike 9.1. Named on Windows alone because Windows is where Desktop was run; the same
|
|
7475
|
+
// defect on another platform's Desktop build would be a guess until someone watches it.
|
|
7476
|
+
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"
|
|
7477
|
+
},
|
|
6054
7478
|
{ os: "linux", wsl: true, supported: true, limitation: null },
|
|
6055
7479
|
{ os: "macos", wsl: false, supported: true, limitation: null },
|
|
6056
7480
|
{ os: "linux", wsl: false, supported: true, limitation: null }
|
|
@@ -6060,6 +7484,31 @@ var MANIFEST5 = {
|
|
|
6060
7484
|
harness: CLAUDE3,
|
|
6061
7485
|
testedVersions: { minimum: "2.0.0", maximum: "2.1.212" },
|
|
6062
7486
|
verificationTier: "canary"
|
|
7487
|
+
},
|
|
7488
|
+
{
|
|
7489
|
+
harness: OPENCODE3,
|
|
7490
|
+
// Spike 9.1 watched interception on the CLI at `1.18.11` and watched it fail on Desktop at
|
|
7491
|
+
// `1.18.14`. The range covers both because both were observed; the failure is a declared
|
|
7492
|
+
// platform limitation below rather than a narrower range, because it is a property of the
|
|
7493
|
+
// host application and not of the OpenCode version — the same 1.18.x plugin loads in both.
|
|
7494
|
+
testedVersions: { minimum: "1.18.11", maximum: "1.18.14" },
|
|
7495
|
+
/**
|
|
7496
|
+
* `config-only`, even though spike 9.1 watched a canary-grade interception here — the counter
|
|
7497
|
+
* moved by exactly the number of shell calls the OpenCode session made.
|
|
7498
|
+
*
|
|
7499
|
+
* A tier is what `verify` can prove on the user's machine, not what a spike proved once on a
|
|
7500
|
+
* clean one. RTK's receipt is its history database, and the `commands` table carries
|
|
7501
|
+
* `timestamp`, token counts, `exec_time_ms`, and `project_path` — no harness column, and no
|
|
7502
|
+
* column standing in for one. The spike could attribute those rows because it ran OpenCode
|
|
7503
|
+
* alone in a scratch directory and read the counter either side; `verify` runs where both
|
|
7504
|
+
* harnesses are wired to the same binary and writing into the same table, and cannot say
|
|
7505
|
+
* which of them a row came from.
|
|
7506
|
+
*
|
|
7507
|
+
* Declaring `canary` here would credit RTK's whole command history to whichever harness the
|
|
7508
|
+
* reader happened to be looking at. Claude Code keeps `canary` because its own hook receipt
|
|
7509
|
+
* is per-harness; this one is provider-wide, and RFC 0007 asks the question per harness.
|
|
7510
|
+
*/
|
|
7511
|
+
verificationTier: "config-only"
|
|
6063
7512
|
}
|
|
6064
7513
|
],
|
|
6065
7514
|
installationChannels: [
|
|
@@ -6101,11 +7550,12 @@ var MANIFEST5 = {
|
|
|
6101
7550
|
// location differs per platform; `rtkDatabasePath` is the single derivation.
|
|
6102
7551
|
locations: ["<user data directory>/rtk/history.db"]
|
|
6103
7552
|
},
|
|
6104
|
-
|
|
7553
|
+
delegatedInstallReviews: null
|
|
6105
7554
|
};
|
|
6106
|
-
var TESTED_VERSIONS2 = { minimum: "0.40.0", maximum: "0.
|
|
6107
|
-
var
|
|
7555
|
+
var TESTED_VERSIONS2 = { minimum: "0.40.0", maximum: "0.44.0" };
|
|
7556
|
+
var VERSION_PATTERN7 = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/;
|
|
6108
7557
|
var HOOK_COMMAND_PATTERN2 = /(^|[\\/\s"'])rtk(\.exe)?([\s"']|$)/i;
|
|
7558
|
+
var PLUGIN_MODULE_PATTERN3 = /(^|[\\/])rtk\.(ts|js|mjs|cjs|mts|cts)$/i;
|
|
6109
7559
|
function isRecord4(value) {
|
|
6110
7560
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6111
7561
|
}
|
|
@@ -6148,7 +7598,7 @@ function parseRtkAnalytics(text) {
|
|
|
6148
7598
|
function harnessesWiredToRtk(configs) {
|
|
6149
7599
|
const wired = /* @__PURE__ */ new Set();
|
|
6150
7600
|
for (const config of configs) {
|
|
6151
|
-
if (config.commands.some((command) =>
|
|
7601
|
+
if (config.commands.some((command) => identifiesCommand2(command))) {
|
|
6152
7602
|
wired.add(config.harnessId);
|
|
6153
7603
|
}
|
|
6154
7604
|
}
|
|
@@ -6175,17 +7625,17 @@ async function readVersion(context) {
|
|
|
6175
7625
|
]
|
|
6176
7626
|
};
|
|
6177
7627
|
}
|
|
6178
|
-
const
|
|
7628
|
+
const version3 = VERSION_PATTERN7.exec(outcome2.stdout)?.[1] ?? null;
|
|
6179
7629
|
return {
|
|
6180
|
-
version:
|
|
6181
|
-
verdict:
|
|
7630
|
+
version: version3,
|
|
7631
|
+
verdict: version3 === null ? null : classifyVersion(version3, TESTED_VERSIONS2),
|
|
6182
7632
|
executable: outcome2.executablePath,
|
|
6183
7633
|
evidence: [
|
|
6184
7634
|
evidence({
|
|
6185
7635
|
kind: "version-output",
|
|
6186
7636
|
source: "rtk --version",
|
|
6187
7637
|
path: outcome2.executablePath,
|
|
6188
|
-
detail:
|
|
7638
|
+
detail: version3 === null ? "reported no recognisable version" : `reported ${version3}`
|
|
6189
7639
|
})
|
|
6190
7640
|
]
|
|
6191
7641
|
};
|
|
@@ -6221,11 +7671,11 @@ async function readAnalytics(context) {
|
|
|
6221
7671
|
]
|
|
6222
7672
|
};
|
|
6223
7673
|
}
|
|
6224
|
-
async function
|
|
7674
|
+
async function detect7(context) {
|
|
6225
7675
|
const evidenceItems = [];
|
|
6226
7676
|
const warnings = [];
|
|
6227
|
-
const
|
|
6228
|
-
evidenceItems.push(...
|
|
7677
|
+
const version3 = await readVersion(context);
|
|
7678
|
+
evidenceItems.push(...version3.evidence);
|
|
6229
7679
|
const configured = harnessesWiredToRtk(context.harnessConfigs);
|
|
6230
7680
|
for (const harness of configured) {
|
|
6231
7681
|
evidenceItems.push(evidence({
|
|
@@ -6235,15 +7685,15 @@ async function detect5(context) {
|
|
|
6235
7685
|
detail: "names rtk in a hook command"
|
|
6236
7686
|
}));
|
|
6237
7687
|
}
|
|
6238
|
-
if (
|
|
7688
|
+
if (version3.verdict === "unknown-newer") {
|
|
6239
7689
|
warnings.push(diagnostic({
|
|
6240
7690
|
severity: "warning",
|
|
6241
7691
|
code: "provider-version-unknown-newer",
|
|
6242
|
-
message: `RTK ${String(
|
|
7692
|
+
message: `RTK ${String(version3.version)} is newer than the versions Token Harness has observed (up to ${TESTED_VERSIONS2.maximum})`,
|
|
6243
7693
|
remediation: "Check the release notes for analytics or hook changes before applying a plan"
|
|
6244
7694
|
}));
|
|
6245
7695
|
}
|
|
6246
|
-
const state =
|
|
7696
|
+
const state = version3.version === null ? configured.length > 0 ? "broken" : "absent" : configured.length > 0 ? "configured" : "installed";
|
|
6247
7697
|
if (state === "broken") {
|
|
6248
7698
|
warnings.push(diagnostic({
|
|
6249
7699
|
severity: "error",
|
|
@@ -6256,36 +7706,61 @@ async function detect5(context) {
|
|
|
6256
7706
|
return {
|
|
6257
7707
|
providerId: RTK2,
|
|
6258
7708
|
state,
|
|
6259
|
-
version:
|
|
6260
|
-
executable:
|
|
7709
|
+
version: version3.version,
|
|
7710
|
+
executable: version3.executable,
|
|
6261
7711
|
// Detection reports what it saw; which channel installed it is an installation-stage
|
|
6262
7712
|
// question and guessing it from a path would be a guess.
|
|
6263
7713
|
installationChannel: null,
|
|
6264
|
-
versionVerdict:
|
|
7714
|
+
versionVerdict: version3.verdict,
|
|
6265
7715
|
configuredHarnesses: configured,
|
|
6266
7716
|
unmanagedHarnessesConfigured: [],
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
7717
|
+
/**
|
|
7718
|
+
* RFC 0002 §Providers may exceed the managed surface.
|
|
7719
|
+
*
|
|
7720
|
+
* The field asks whether this manifest names a harness with no adapter behind it. It names two,
|
|
7721
|
+
* Claude Code and OpenCode, and both are managed — so the qualifier RFC 0006's doctor transcript
|
|
7722
|
+
* uses for HarnessTrim does not apply, and the report says "not configured for any harness"
|
|
7723
|
+
* without hedging.
|
|
7724
|
+
*
|
|
7725
|
+
* `rtk init --agent` separately targets Cursor, Windsurf, Cline, Kilo Code, Antigravity, Kimi,
|
|
7726
|
+
* Pi, Hermes, and Droid. That is RTK exceeding *Token Harness*, not this manifest exceeding the
|
|
7727
|
+
* managed set, and it stays out of the flag: claiming those here would make the resolver
|
|
7728
|
+
* responsible for harnesses this build has no adapter to inspect.
|
|
7729
|
+
*/
|
|
6270
7730
|
supportsUnmanagedHarnesses: false,
|
|
6271
7731
|
// RFC 0004 §Brownfield adoption: nothing has been applied by Token Harness yet, so
|
|
6272
7732
|
// every installation it finds is the user's.
|
|
6273
7733
|
managedByTokenHarness: false,
|
|
7734
|
+
/**
|
|
7735
|
+
* Claude Code alone, and independent of the observed version.
|
|
7736
|
+
*
|
|
7737
|
+
* RTK's assignment is produced by this build writing the hook itself — `rtk-plan.ts` appends the
|
|
7738
|
+
* entry — so nothing about the installed `rtk` decides whether the state is reachable. What does
|
|
7739
|
+
* decide it is which harness schema that builder knows, and it knows one: `HOOK_LIST_HARNESSES`
|
|
7740
|
+
* holds `claude` and the builder refuses anything else, because a `{matcher, hooks:[…]}` object
|
|
7741
|
+
* at `hooks.<event>` is Claude Code's schema and nothing else's.
|
|
7742
|
+
*
|
|
7743
|
+
* RTK claims OpenCode too, from spike 9.1, and that claim is real — detected, adopted, verified
|
|
7744
|
+
* and measured there. It is simply not *written* there: its OpenCode integration is a plugin
|
|
7745
|
+
* module `rtk init -g --opencode` installs globally, which this build has no action for. Saying
|
|
7746
|
+
* so here is what stops the resolver assigning a scope nothing can produce.
|
|
7747
|
+
*/
|
|
7748
|
+
assignableHarnesses: [CLAUDE3],
|
|
6274
7749
|
evidence: evidenceItems,
|
|
6275
7750
|
warnings
|
|
6276
7751
|
};
|
|
6277
7752
|
}
|
|
6278
|
-
async function
|
|
7753
|
+
async function verify7(context) {
|
|
6279
7754
|
const checks = [];
|
|
6280
7755
|
const diagnostics = [];
|
|
6281
|
-
const
|
|
7756
|
+
const version3 = await readVersion(context);
|
|
6282
7757
|
checks.push({
|
|
6283
7758
|
id: "executable-resolves",
|
|
6284
|
-
status:
|
|
6285
|
-
summary:
|
|
6286
|
-
achievedTier:
|
|
6287
|
-
evidence:
|
|
6288
|
-
remediation:
|
|
7759
|
+
status: version3.version === null ? "fail" : "pass",
|
|
7760
|
+
summary: version3.version === null ? "rtk could not be run" : `rtk ${version3.version}`,
|
|
7761
|
+
achievedTier: version3.version === null ? null : "presence",
|
|
7762
|
+
evidence: version3.evidence,
|
|
7763
|
+
remediation: version3.version === null ? "Install RTK, or add it to PATH" : null
|
|
6289
7764
|
});
|
|
6290
7765
|
const configured = harnessesWiredToRtk(context.harnessConfigs);
|
|
6291
7766
|
checks.push({
|
|
@@ -6392,7 +7867,7 @@ function toEvent2(row2, context) {
|
|
|
6392
7867
|
context: {
|
|
6393
7868
|
// An empty `project_path` means RTK recorded no directory; attributing the event to
|
|
6394
7869
|
// the directory `metrics` happens to run in would invent an attribution.
|
|
6395
|
-
projectId: projectPath === null || projectPath === "" ?
|
|
7870
|
+
projectId: projectPath === null || projectPath === "" ? UNATTRIBUTED_PROJECT_ID : context.projectIdFor(projectPath),
|
|
6396
7871
|
// RTK proxies commands for whichever harness invoked it and does not record which.
|
|
6397
7872
|
// The hook that wires it is per harness, but a *row* carries no harness, and reading
|
|
6398
7873
|
// one off the current configuration would attribute months of history to today's
|
|
@@ -6582,22 +8057,22 @@ async function collectMetrics2(context, store) {
|
|
|
6582
8057
|
}
|
|
6583
8058
|
var SOURCE_LABEL = "rtk history.db (commands)";
|
|
6584
8059
|
function identifiesCommand2(command) {
|
|
6585
|
-
return HOOK_COMMAND_PATTERN2.test(command);
|
|
8060
|
+
return HOOK_COMMAND_PATTERN2.test(command) || PLUGIN_MODULE_PATTERN3.test(command);
|
|
6586
8061
|
}
|
|
6587
8062
|
async function plan2(context, request) {
|
|
6588
|
-
const
|
|
8063
|
+
const version3 = await readVersion(context);
|
|
6589
8064
|
return buildRtkPlan({
|
|
6590
8065
|
context,
|
|
6591
8066
|
request,
|
|
6592
|
-
installed:
|
|
8067
|
+
installed: version3.version !== null,
|
|
6593
8068
|
identifiesCommand: identifiesCommand2,
|
|
6594
|
-
installationChannels:
|
|
8069
|
+
installationChannels: MANIFEST7.installationChannels
|
|
6595
8070
|
});
|
|
6596
8071
|
}
|
|
6597
8072
|
var rtkAdapter = {
|
|
6598
|
-
manifest:
|
|
6599
|
-
detect:
|
|
6600
|
-
verify:
|
|
8073
|
+
manifest: MANIFEST7,
|
|
8074
|
+
detect: detect7,
|
|
8075
|
+
verify: verify7,
|
|
6601
8076
|
collectMetrics: collectMetrics2,
|
|
6602
8077
|
identifiesCommand: identifiesCommand2,
|
|
6603
8078
|
plan: plan2
|
|
@@ -6611,7 +8086,6 @@ function listProviderAdapters() {
|
|
|
6611
8086
|
|
|
6612
8087
|
// apps/cli/dist/src/commands/plan.js
|
|
6613
8088
|
var DEFAULT_PROFILE = "safe";
|
|
6614
|
-
var ASSIGNABLE_PROVIDERS = ["rtk"];
|
|
6615
8089
|
async function computePlan(context) {
|
|
6616
8090
|
const diagnostics = [];
|
|
6617
8091
|
const providerAdapters = listProviderAdapters().filter((adapter) => context.provider === null || adapter.manifest.id === context.provider);
|
|
@@ -6638,11 +8112,6 @@ async function computePlan(context) {
|
|
|
6638
8112
|
harnessConfigs.push(...inspection.summaries);
|
|
6639
8113
|
}
|
|
6640
8114
|
}
|
|
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
8115
|
const providerContext = detectionContext === null || context.adapters === null ? null : {
|
|
6647
8116
|
...detectionContext,
|
|
6648
8117
|
harnessConfigs,
|
|
@@ -6650,12 +8119,19 @@ async function computePlan(context) {
|
|
|
6650
8119
|
localDatabase: context.adapters.localDatabase,
|
|
6651
8120
|
projectIdFor: context.adapters.projectIdFor
|
|
6652
8121
|
};
|
|
8122
|
+
const assignable = /* @__PURE__ */ new Map();
|
|
6653
8123
|
if (providerContext !== null) {
|
|
6654
8124
|
for (const adapter of providerAdapters) {
|
|
6655
8125
|
const detection = await adapter.detect(providerContext);
|
|
6656
8126
|
versions.providers[adapter.manifest.id] = detection.version;
|
|
8127
|
+
assignable.set(adapter.manifest.id, new Set(detection.assignableHarnesses));
|
|
6657
8128
|
}
|
|
6658
8129
|
}
|
|
8130
|
+
const providers = providerAdapters.map((adapter) => ({
|
|
8131
|
+
id: adapter.manifest.id,
|
|
8132
|
+
capabilities: adapter.manifest.capabilities,
|
|
8133
|
+
assignableHarnesses: assignable.get(adapter.manifest.id) ?? /* @__PURE__ */ new Set()
|
|
8134
|
+
}));
|
|
6659
8135
|
const resolution = resolveOwnership({
|
|
6660
8136
|
profile,
|
|
6661
8137
|
harnesses: present,
|
|
@@ -6665,7 +8141,9 @@ async function computePlan(context) {
|
|
|
6665
8141
|
harness: context.harness
|
|
6666
8142
|
});
|
|
6667
8143
|
const actions = [];
|
|
8144
|
+
const blocked = [];
|
|
6668
8145
|
if (providerContext !== null) {
|
|
8146
|
+
const rows = context.compatibilityRows ?? COMPATIBILITY_ROWS;
|
|
6669
8147
|
for (const adapter of providerAdapters) {
|
|
6670
8148
|
const owned = resolution.ownership.filter((entry) => entry.owner === adapter.manifest.id);
|
|
6671
8149
|
if (owned.length === 0 && adapter.plansWithoutOwnership !== true)
|
|
@@ -6675,7 +8153,35 @@ async function computePlan(context) {
|
|
|
6675
8153
|
harnesses: present,
|
|
6676
8154
|
desiredState: "configured"
|
|
6677
8155
|
});
|
|
6678
|
-
|
|
8156
|
+
if (providerPlan.actions.length === 0)
|
|
8157
|
+
continue;
|
|
8158
|
+
const touched = [...new Set(owned.map((entry) => entry.scope.harness))];
|
|
8159
|
+
if (touched.length === 0)
|
|
8160
|
+
touched.push(...present.map((harness) => harness.id));
|
|
8161
|
+
let admitted = true;
|
|
8162
|
+
for (const harness of touched) {
|
|
8163
|
+
const admission = admitManagedMutation(rows, {
|
|
8164
|
+
provider: adapter.manifest.id,
|
|
8165
|
+
providerVersion: versions.providers[adapter.manifest.id] ?? null,
|
|
8166
|
+
harness,
|
|
8167
|
+
harnessVersion: versions.harnesses[harness] ?? null,
|
|
8168
|
+
os: context.platform.os,
|
|
8169
|
+
wsl: context.platform.isWsl
|
|
8170
|
+
});
|
|
8171
|
+
if (admission.state !== "admitted") {
|
|
8172
|
+
admitted = false;
|
|
8173
|
+
blocked.push({
|
|
8174
|
+
provider: adapter.manifest.id,
|
|
8175
|
+
providerVersion: versions.providers[adapter.manifest.id] ?? null,
|
|
8176
|
+
harness,
|
|
8177
|
+
harnessVersion: versions.harnesses[harness] ?? null,
|
|
8178
|
+
verdict: admission.verdict,
|
|
8179
|
+
missing: admission.missing
|
|
8180
|
+
});
|
|
8181
|
+
}
|
|
8182
|
+
}
|
|
8183
|
+
if (admitted)
|
|
8184
|
+
actions.push(...providerPlan.actions);
|
|
6679
8185
|
}
|
|
6680
8186
|
}
|
|
6681
8187
|
const report = {
|
|
@@ -6733,7 +8239,7 @@ async function computePlan(context) {
|
|
|
6733
8239
|
remediation: null
|
|
6734
8240
|
}));
|
|
6735
8241
|
}
|
|
6736
|
-
if (resolution.ownership.length > 0 && actions.length === 0) {
|
|
8242
|
+
if (resolution.ownership.length > 0 && actions.length === 0 && blocked.length === 0) {
|
|
6737
8243
|
diagnostics.push(diagnostic({
|
|
6738
8244
|
severity: "info",
|
|
6739
8245
|
code: "already-in-desired-state",
|
|
@@ -6741,6 +8247,15 @@ async function computePlan(context) {
|
|
|
6741
8247
|
remediation: null
|
|
6742
8248
|
}));
|
|
6743
8249
|
}
|
|
8250
|
+
const refusalIsTheOutcome = actions.length === 0;
|
|
8251
|
+
for (const entry of blocked) {
|
|
8252
|
+
diagnostics.push(diagnostic({
|
|
8253
|
+
severity: refusalIsTheOutcome ? "error" : "warning",
|
|
8254
|
+
code: "managed-mutation-blocked",
|
|
8255
|
+
message: `plan refuses managed mutation of ${entry.harness} by ${entry.provider}${entry.providerVersion !== null ? ` at ${entry.providerVersion}` : ""}: ${entry.missing}`,
|
|
8256
|
+
remediation: "Add a compatibility row whose fixture proves this combination, or configure this integration by hand"
|
|
8257
|
+
}));
|
|
8258
|
+
}
|
|
6744
8259
|
for (const conflict of resolution.conflicts) {
|
|
6745
8260
|
diagnostics.push(diagnostic({
|
|
6746
8261
|
severity: "error",
|
|
@@ -6749,7 +8264,7 @@ async function computePlan(context) {
|
|
|
6749
8264
|
remediation: conflict.remediation
|
|
6750
8265
|
}));
|
|
6751
8266
|
}
|
|
6752
|
-
return { report, versions, diagnostics, present, harnessConfigs };
|
|
8267
|
+
return { report, versions, diagnostics, present, harnessConfigs, blocked };
|
|
6753
8268
|
}
|
|
6754
8269
|
async function persist(context, computed) {
|
|
6755
8270
|
if (context.adapters === null || context.stateRoot === null)
|
|
@@ -6774,7 +8289,11 @@ async function runPlan(context) {
|
|
|
6774
8289
|
const computed = await computePlan(context);
|
|
6775
8290
|
const persisted = await persist(context, computed);
|
|
6776
8291
|
const report = { ...computed.report, persisted };
|
|
6777
|
-
const exitCode =
|
|
8292
|
+
const exitCode = planExitCode({
|
|
8293
|
+
blocked: computed.blocked.length,
|
|
8294
|
+
actions: report.actions.length,
|
|
8295
|
+
conflicts: report.conflicts.length
|
|
8296
|
+
});
|
|
6778
8297
|
return commandResult({
|
|
6779
8298
|
command: "plan",
|
|
6780
8299
|
exitCode,
|
|
@@ -6782,6 +8301,13 @@ async function runPlan(context) {
|
|
|
6782
8301
|
diagnostics: computed.diagnostics
|
|
6783
8302
|
});
|
|
6784
8303
|
}
|
|
8304
|
+
function planExitCode(counts) {
|
|
8305
|
+
if (counts.blocked > 0 && counts.actions === 0)
|
|
8306
|
+
return EXIT_CODES["unsupported-environment"];
|
|
8307
|
+
if (counts.conflicts > 0)
|
|
8308
|
+
return EXIT_CODES["blocked-by-conflict"];
|
|
8309
|
+
return EXIT_CODES.ok;
|
|
8310
|
+
}
|
|
6785
8311
|
|
|
6786
8312
|
// apps/cli/dist/src/commands/apply.js
|
|
6787
8313
|
var PLANS_DIRECTORY = "plans";
|
|
@@ -6845,6 +8371,9 @@ async function runApply(context) {
|
|
|
6845
8371
|
const diagnostics = [];
|
|
6846
8372
|
const computed = await computePlan(context);
|
|
6847
8373
|
diagnostics.push(...computed.diagnostics);
|
|
8374
|
+
if (computed.blocked.length > 0 && computed.report.actions.length === 0) {
|
|
8375
|
+
return finish("rejected", EXIT_CODES["unsupported-environment"], emptyReport("rejected"), diagnostics);
|
|
8376
|
+
}
|
|
6848
8377
|
if (computed.report.conflicts.length > 0) {
|
|
6849
8378
|
return finish("rejected", EXIT_CODES["blocked-by-conflict"], emptyReport("rejected"), diagnostics);
|
|
6850
8379
|
}
|
|
@@ -6999,10 +8528,40 @@ async function runDoctor(context) {
|
|
|
6999
8528
|
projectIdFor: context.adapters?.projectIdFor ?? (() => "p_unattributed")
|
|
7000
8529
|
})));
|
|
7001
8530
|
const problemCount = harnesses.filter(isBroken).length + providers.filter(isBroken).length + [...harnesses, ...providers].filter((detection) => detection.versionVerdict === "unknown-newer").length;
|
|
8531
|
+
const rows = context.compatibilityRows ?? COMPATIBILITY_ROWS;
|
|
8532
|
+
const providersWithCoverage = providers.map((detection) => {
|
|
8533
|
+
if (detection.state !== "configured" || detection.configuredHarnesses.length === 0) {
|
|
8534
|
+
return detection;
|
|
8535
|
+
}
|
|
8536
|
+
const uncovered = detection.configuredHarnesses.flatMap((harness) => {
|
|
8537
|
+
const harnessVersion = harnesses.find((candidate) => candidate.harnessId === harness)?.version ?? null;
|
|
8538
|
+
const admission = admitManagedMutation(rows, {
|
|
8539
|
+
provider: detection.providerId,
|
|
8540
|
+
providerVersion: detection.version,
|
|
8541
|
+
harness,
|
|
8542
|
+
harnessVersion,
|
|
8543
|
+
os: context.platform.os,
|
|
8544
|
+
wsl: context.platform.isWsl
|
|
8545
|
+
});
|
|
8546
|
+
if (admission.state === "admitted")
|
|
8547
|
+
return [];
|
|
8548
|
+
return [
|
|
8549
|
+
diagnostic({
|
|
8550
|
+
severity: "warning",
|
|
8551
|
+
code: "no-compatibility-row",
|
|
8552
|
+
message: `no compatibility row covers ${detection.providerId} on ${harness}${detection.version !== null ? ` at ${detection.version}` : ""} \u2014 ${admission.missing}`,
|
|
8553
|
+
remediation: "Add a compatibility row whose fixture proves this combination, or configure this integration by hand"
|
|
8554
|
+
})
|
|
8555
|
+
];
|
|
8556
|
+
});
|
|
8557
|
+
if (uncovered.length === 0)
|
|
8558
|
+
return detection;
|
|
8559
|
+
return { ...detection, warnings: [...detection.warnings, ...uncovered] };
|
|
8560
|
+
});
|
|
7002
8561
|
const report = {
|
|
7003
8562
|
platform: context.platform,
|
|
7004
8563
|
harnesses,
|
|
7005
|
-
providers,
|
|
8564
|
+
providers: providersWithCoverage,
|
|
7006
8565
|
problemCount
|
|
7007
8566
|
};
|
|
7008
8567
|
return commandResult({
|
|
@@ -7094,14 +8653,42 @@ async function runMetrics(context) {
|
|
|
7094
8653
|
}
|
|
7095
8654
|
}
|
|
7096
8655
|
}
|
|
8656
|
+
const projectId = context.adapters === null ? null : context.adapters.projectIdFor(context.projectRoot);
|
|
7097
8657
|
const events = [];
|
|
7098
8658
|
for await (const event of store.query({
|
|
7099
8659
|
since: window.sinceInstant,
|
|
7100
8660
|
until: window.untilInstant,
|
|
8661
|
+
...projectId === null ? {} : { projectId },
|
|
7101
8662
|
...context.provider === null ? {} : { providerIds: [context.provider] }
|
|
7102
8663
|
})) {
|
|
7103
8664
|
events.push(event);
|
|
7104
8665
|
}
|
|
8666
|
+
if (projectId === null) {
|
|
8667
|
+
diagnostics.push(diagnostic({
|
|
8668
|
+
severity: "info",
|
|
8669
|
+
code: "metrics-not-project-scoped",
|
|
8670
|
+
message: "This report covers every project in the store: no project identity was available",
|
|
8671
|
+
remediation: null
|
|
8672
|
+
}));
|
|
8673
|
+
} else {
|
|
8674
|
+
let unattributed = 0;
|
|
8675
|
+
for await (const _event of store.query({
|
|
8676
|
+
since: window.sinceInstant,
|
|
8677
|
+
until: window.untilInstant,
|
|
8678
|
+
projectId: UNATTRIBUTED_PROJECT_ID,
|
|
8679
|
+
...context.provider === null ? {} : { providerIds: [context.provider] }
|
|
8680
|
+
})) {
|
|
8681
|
+
unattributed += 1;
|
|
8682
|
+
}
|
|
8683
|
+
if (unattributed > 0) {
|
|
8684
|
+
diagnostics.push(diagnostic({
|
|
8685
|
+
severity: "info",
|
|
8686
|
+
code: "metrics-unattributed-excluded",
|
|
8687
|
+
message: `${String(unattributed)} operation${unattributed === 1 ? "" : "s"} named no project and are excluded from this report`,
|
|
8688
|
+
remediation: null
|
|
8689
|
+
}));
|
|
8690
|
+
}
|
|
8691
|
+
}
|
|
7105
8692
|
const report = aggregateEvents({
|
|
7106
8693
|
events,
|
|
7107
8694
|
windowStart: window.windowStart,
|
|
@@ -7226,7 +8813,12 @@ async function runRollback(context) {
|
|
|
7226
8813
|
fs,
|
|
7227
8814
|
snapshots: built.store,
|
|
7228
8815
|
journal: built.journal,
|
|
7229
|
-
now: context.now
|
|
8816
|
+
now: context.now,
|
|
8817
|
+
// Package inventories, when the transaction captured any: restoring a package needs the same
|
|
8818
|
+
// process runner the install used. Absent, the rollback reports those packages as not
|
|
8819
|
+
// restored instead of attempting a silent reinstall.
|
|
8820
|
+
runner: context.adapters.runner,
|
|
8821
|
+
cwd: context.projectRoot
|
|
7230
8822
|
});
|
|
7231
8823
|
diagnostics.push(...result.diagnostics);
|
|
7232
8824
|
const report = empty(result.exitCode === 0 ? "rolled-back" : "dirty");
|
|
@@ -7348,7 +8940,6 @@ async function runUninstall(context) {
|
|
|
7348
8940
|
}
|
|
7349
8941
|
|
|
7350
8942
|
// apps/cli/dist/src/commands/status.js
|
|
7351
|
-
var ASSIGNABLE_PROVIDERS2 = ["rtk"];
|
|
7352
8943
|
async function runStatus(context) {
|
|
7353
8944
|
const report = {
|
|
7354
8945
|
platform: context.platform,
|
|
@@ -7386,12 +8977,8 @@ async function runStatus(context) {
|
|
|
7386
8977
|
configs.push(...inspection.summaries);
|
|
7387
8978
|
}
|
|
7388
8979
|
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
8980
|
const observedVersions = {};
|
|
8981
|
+
const providers = [];
|
|
7395
8982
|
for (const adapter of providerAdapters) {
|
|
7396
8983
|
const detection = await adapter.detect({
|
|
7397
8984
|
...detectionContext,
|
|
@@ -7401,6 +8988,11 @@ async function runStatus(context) {
|
|
|
7401
8988
|
projectIdFor: context.adapters.projectIdFor
|
|
7402
8989
|
});
|
|
7403
8990
|
observedVersions[adapter.manifest.id] = detection.version;
|
|
8991
|
+
providers.push({
|
|
8992
|
+
id: adapter.manifest.id,
|
|
8993
|
+
capabilities: adapter.manifest.capabilities,
|
|
8994
|
+
assignableHarnesses: new Set(detection.assignableHarnesses)
|
|
8995
|
+
});
|
|
7404
8996
|
}
|
|
7405
8997
|
const resolution = resolveOwnership({
|
|
7406
8998
|
profile: "safe",
|
|
@@ -7461,14 +9053,15 @@ function upgradeAction(input) {
|
|
|
7461
9053
|
],
|
|
7462
9054
|
postconditions: [`${input.packageName} reports ${input.target}`],
|
|
7463
9055
|
/**
|
|
7464
|
-
* `
|
|
9056
|
+
* `package-inventory` where the channel can report what is installed, `none` otherwise.
|
|
7465
9057
|
*
|
|
7466
9058
|
* 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
|
-
*
|
|
9059
|
+
* version in place, so the executor's receipt is not boilerplate here — it is the difference
|
|
9060
|
+
* between the machine being restored and the *configuration* being restored. With an
|
|
9061
|
+
* inventory-capturing channel the rollback also reinstalls the previous version, and the
|
|
9062
|
+
* receipt says per package whether that reinstall took.
|
|
7470
9063
|
*/
|
|
7471
|
-
rollbackData: "none",
|
|
9064
|
+
rollbackData: channelCanReportInventory(input.channel) ? "package-inventory" : "none",
|
|
7472
9065
|
explanation: `Update ${input.packageName} from ${input.installed} to ${input.target} through ${input.channel}`,
|
|
7473
9066
|
packageManager: input.channel,
|
|
7474
9067
|
packageName: input.packageName,
|
|
@@ -7741,11 +9334,12 @@ async function runVerify(context) {
|
|
|
7741
9334
|
presentHarnesses
|
|
7742
9335
|
);
|
|
7743
9336
|
for (const harnessId2 of harnesses) {
|
|
9337
|
+
const declaredTier = adapter.manifest.harnesses.find((entry) => entry.harness === harnessId2)?.verificationTier ?? verification.declaredTier;
|
|
7744
9338
|
results.push({
|
|
7745
9339
|
providerId: adapter.manifest.id,
|
|
7746
9340
|
harnessId: harnessId2,
|
|
7747
|
-
status: statusFor(verification.achievedTier,
|
|
7748
|
-
declaredTier
|
|
9341
|
+
status: statusFor(verification.achievedTier, declaredTier),
|
|
9342
|
+
declaredTier,
|
|
7749
9343
|
managedByTokenHarness: MANAGED_PROVIDERS.includes(adapter.manifest.id),
|
|
7750
9344
|
checks: verification.checks
|
|
7751
9345
|
});
|
|
@@ -7942,7 +9536,8 @@ function renderApplyReport(report, context, command = "apply") {
|
|
|
7942
9536
|
lines.push(` ${displayPath(path, context.home)}`);
|
|
7943
9537
|
lines.push("");
|
|
7944
9538
|
}
|
|
7945
|
-
|
|
9539
|
+
const removedNothing = command === "uninstall" && report.outcome === "committed" && report.results.length > 0 && report.results.every((result) => result.status !== "applied");
|
|
9540
|
+
lines.push(removedNothing ? "Removed nothing: no owned entry was recognised. Everything is untouched." : (OUTCOME_LINES[command] ?? OUTCOME_LINES["apply"])?.[report.outcome] ?? "");
|
|
7946
9541
|
return document(lines);
|
|
7947
9542
|
}
|
|
7948
9543
|
|
|
@@ -8609,7 +10204,7 @@ function usageText(topic) {
|
|
|
8609
10204
|
}
|
|
8610
10205
|
|
|
8611
10206
|
// apps/cli/dist/src/version.js
|
|
8612
|
-
var TOOL_VERSION = "0.1.
|
|
10207
|
+
var TOOL_VERSION = "0.1.3";
|
|
8613
10208
|
|
|
8614
10209
|
// apps/cli/dist/src/run.js
|
|
8615
10210
|
var DEFAULT_COMMANDS = {
|
|
@@ -8786,7 +10381,8 @@ async function run(options) {
|
|
|
8786
10381
|
until: invocation.options.until,
|
|
8787
10382
|
planId: invocation.options.plan,
|
|
8788
10383
|
confirmed: invocation.options.yes,
|
|
8789
|
-
metrics: options.metrics ?? null
|
|
10384
|
+
metrics: options.metrics ?? null,
|
|
10385
|
+
compatibilityRows: options.compatibilityRows ?? null
|
|
8790
10386
|
};
|
|
8791
10387
|
const table = options.commands ?? DEFAULT_COMMANDS;
|
|
8792
10388
|
let result;
|