specpi 0.29.0 → 0.30.1
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/CHANGELOG.md +46 -0
- package/NPM_RELEASE.md +2 -2
- package/README.md +22 -24
- package/SECURITY_MODEL.md +10 -6
- package/extensions/jev-advisor/broker.mjs +89 -11
- package/extensions/jev-advisor/client.mjs +33 -10
- package/extensions/jev-advisor/config.mjs +32 -17
- package/extensions/jev-advisor/consent.mjs +19 -7
- package/extensions/jev-advisor/gate.mjs +25 -21
- package/extensions/jev-advisor/index.ts +308 -300
- package/extensions/jev-advisor/ledger.mjs +16 -2
- package/extensions/jev-advisor/questions/gap.mjs +49 -86
- package/extensions/jev-advisor/questions/retention.mjs +5 -8
- package/extensions/jev-advisor/questions/sources.mjs +27 -56
- package/extensions/jev-advisor/sanitize.mjs +0 -0
- package/extensions/tool-wishlist/core.mjs +86 -0
- package/extensions/tool-wishlist/index.ts +81 -8
- package/extensions/workflow-controls/index.ts +23 -0
- package/package.json +2 -2
- package/scripts/specpi.mjs +0 -1
- package/extensions/jev-advisor/questions/compaction.mjs +0 -153
|
@@ -20,6 +20,12 @@ import { MAX_STATE_BYTES } from "./sanitize.mjs";
|
|
|
20
20
|
* and a TYPESAFE_BASE_URL override) names itself, and so the rule below is true rather than
|
|
21
21
|
* aspirational: a grant is keyed on this string, so repointing the client really does ask again.
|
|
22
22
|
*/
|
|
23
|
+
export const CONSENT_SCHEMA = 2;
|
|
24
|
+
|
|
25
|
+
export function endpointOrigin() {
|
|
26
|
+
return new URL(baseUrl()).origin;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export function endpointLabel() {
|
|
24
30
|
const base = baseUrl();
|
|
25
31
|
try {
|
|
@@ -43,12 +49,12 @@ export function loadConsent() {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
const stored = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
46
|
-
if (stored?.schema !==
|
|
52
|
+
if (stored?.schema !== CONSENT_SCHEMA || stored.granted !== true || typeof stored.endpoint !== "string") {
|
|
47
53
|
return undefined;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
// A grant is for the endpoint it was given for. Repointing the client asks again.
|
|
51
|
-
return stored.endpoint === endpointLabel() ? stored : undefined;
|
|
57
|
+
return stored.endpoint === endpointLabel() && stored.origin === endpointOrigin() ? stored : undefined;
|
|
52
58
|
} catch {
|
|
53
59
|
return undefined;
|
|
54
60
|
}
|
|
@@ -65,9 +71,11 @@ export function saveConsent() {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const stored = {
|
|
68
|
-
|
|
74
|
+
// Earlier consent incorrectly excluded file/command samples. It cannot authorize them.
|
|
75
|
+
schema: CONSENT_SCHEMA,
|
|
69
76
|
granted: true,
|
|
70
77
|
endpoint: endpointLabel(),
|
|
78
|
+
origin: endpointOrigin(),
|
|
71
79
|
maxStateBytes: MAX_STATE_BYTES,
|
|
72
80
|
grantedAt: new Date().toISOString(),
|
|
73
81
|
};
|
|
@@ -89,15 +97,19 @@ export function consentPath() {
|
|
|
89
97
|
return consentFile();
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
export const CONSENT_TITLE = "Allow SpecPi to send task summaries to Jev?";
|
|
100
|
+
export const CONSENT_TITLE = "Allow SpecPi to send task summaries and text samples to Jev?";
|
|
93
101
|
|
|
94
102
|
export function consentBody(systemLabel) {
|
|
95
103
|
return [
|
|
96
104
|
`${systemLabel} wants to ask TypeSafe's Jev classifier a question about this session.`,
|
|
97
105
|
"",
|
|
98
|
-
`What is sent: a
|
|
99
|
-
|
|
100
|
-
"It
|
|
106
|
+
`What is sent: a state object of at most ${MAX_STATE_BYTES} bytes to ${endpointLabel()}, with classifier questions.`,
|
|
107
|
+
`Transport: ${new URL(baseUrl()).protocol === "https:" ? "HTTPS" : "not HTTPS (configured endpoint override)"}.`,
|
|
108
|
+
"It may include the current request or objective, relative paths, capability-gap summaries,",
|
|
109
|
+
"and short sampled lines from file contents, command output and fetched content.",
|
|
110
|
+
"Known credential, email, URL and outside-workspace path patterns are redacted before sending.",
|
|
111
|
+
"Redaction is best effort, not a guarantee that every sensitive detail is removed. No session history is read.",
|
|
112
|
+
"Collection stays local unless you separately allow this advisor transmission.",
|
|
101
113
|
"",
|
|
102
114
|
"Every call is recorded locally in transmissions.jsonl with a hash of exactly what was sent,",
|
|
103
115
|
"which you can read with /jev ledger. Turn this off at any time with /jev off.",
|
|
@@ -50,12 +50,11 @@
|
|
|
50
50
|
// was unreachable, and running the layer could never have revealed it, because a system that
|
|
51
51
|
// never fires looks exactly like a system whose advice was always to do nothing.
|
|
52
52
|
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// and because a firing there blocks a write.
|
|
53
|
+
// Compaction guidance had the same problem and was lowered to 0.60 to clear it. That system has
|
|
54
|
+
// since been withdrawn -- two tier-6 runs measured the arm carrying it solving fewer long-session
|
|
55
|
+
// tasks than plain SpecPi -- so the loosest gate in the layer is gone with it. gap keeps 0.85
|
|
56
|
+
// because its Noul reaches 0.96 on the case that matters and because a firing there blocks a
|
|
57
|
+
// write.
|
|
59
58
|
|
|
60
59
|
/**
|
|
61
60
|
* The figures the comment above cites, in a form a test can check against the artifact. A citation
|
|
@@ -91,16 +90,6 @@ export const THRESHOLDS = Object.freeze({
|
|
|
91
90
|
// Measured 0.06-0.07 on the spent case, so this clears with room.
|
|
92
91
|
low: 0.1,
|
|
93
92
|
}),
|
|
94
|
-
compaction: Object.freeze({
|
|
95
|
-
scoreConfidence: 0.6,
|
|
96
|
-
boundary: 0.3,
|
|
97
|
-
choiceConfidence: 0.75,
|
|
98
|
-
margin: 0.25,
|
|
99
|
-
// Lowered from 0.85: the clearest open investigation scores 0.63-0.65, and the action is
|
|
100
|
-
// one sentence added to a prompt that is being rebuilt regardless.
|
|
101
|
-
high: 0.6,
|
|
102
|
-
low: 0.15,
|
|
103
|
-
}),
|
|
104
93
|
gap: Object.freeze({
|
|
105
94
|
scoreConfidence: 0.6,
|
|
106
95
|
boundary: 0.3,
|
|
@@ -178,14 +167,14 @@ export function thresholdsFor(system) {
|
|
|
178
167
|
export function nounTrue(answer, system) {
|
|
179
168
|
const limits = thresholdsFor(system);
|
|
180
169
|
|
|
181
|
-
return answer?.kind === "noul" && answer.value >= limits.high;
|
|
170
|
+
return answer?.kind === "noul" && Number.isFinite(answer.value) && answer.value >= limits.high && answer.value <= 1;
|
|
182
171
|
}
|
|
183
172
|
|
|
184
173
|
/** True when the Noul is confidently no. Not the negation of nounTrue: the middle band is silence. */
|
|
185
174
|
export function nounFalse(answer, system) {
|
|
186
175
|
const limits = thresholdsFor(system);
|
|
187
176
|
|
|
188
|
-
return answer?.kind === "noul" && answer.value <= limits.low;
|
|
177
|
+
return answer?.kind === "noul" && Number.isFinite(answer.value) && answer.value >= 0 && answer.value <= limits.low;
|
|
189
178
|
}
|
|
190
179
|
|
|
191
180
|
function topTwo(probabilities) {
|
|
@@ -204,7 +193,15 @@ function topTwo(probabilities) {
|
|
|
204
193
|
* the margin is a live test rather than a branch that silently never runs.
|
|
205
194
|
*/
|
|
206
195
|
export function choiceValue(answer, system) {
|
|
207
|
-
if (
|
|
196
|
+
if (
|
|
197
|
+
answer?.kind !== "choice" ||
|
|
198
|
+
!Number.isFinite(answer.confidence) ||
|
|
199
|
+
answer.confidence > 1 ||
|
|
200
|
+
!answer.probabilities ||
|
|
201
|
+
Array.isArray(answer.probabilities) ||
|
|
202
|
+
!Object.hasOwn(answer.probabilities, answer.value) ||
|
|
203
|
+
!Object.values(answer.probabilities).every((value) => Number.isFinite(value) && value >= 0 && value <= 1)
|
|
204
|
+
) {
|
|
208
205
|
return undefined;
|
|
209
206
|
}
|
|
210
207
|
|
|
@@ -214,7 +211,7 @@ export function choiceValue(answer, system) {
|
|
|
214
211
|
}
|
|
215
212
|
|
|
216
213
|
const { first, second } = topTwo(answer.probabilities);
|
|
217
|
-
if (answer.probabilities
|
|
214
|
+
if (answer.probabilities[answer.value] !== first || first - second < limits.margin) {
|
|
218
215
|
return undefined;
|
|
219
216
|
}
|
|
220
217
|
|
|
@@ -226,7 +223,14 @@ export function choiceValue(answer, system) {
|
|
|
226
223
|
* returned level is the rounded band; callers compare against their own rubric.
|
|
227
224
|
*/
|
|
228
225
|
export function scoreLevel(answer, system) {
|
|
229
|
-
if (
|
|
226
|
+
if (
|
|
227
|
+
answer?.kind !== "score" ||
|
|
228
|
+
!Number.isFinite(answer.confidence) ||
|
|
229
|
+
answer.confidence > 1 ||
|
|
230
|
+
!Number.isFinite(answer.value) ||
|
|
231
|
+
answer.value < 0 ||
|
|
232
|
+
answer.value > 9
|
|
233
|
+
) {
|
|
230
234
|
return undefined;
|
|
231
235
|
}
|
|
232
236
|
|