@golden-frijoles/kit 0.4.0 → 0.5.0
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/dist/lib/jev.mjs +41 -6
- package/dist/lib/prose-guard.mjs +3 -1
- package/package.json +1 -1
package/dist/lib/jev.mjs
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
// 3. ONE COMMITTED SWITCH. `jev.config.json → rails.<rail>.mode: off | shadow | jev`. A missing file is
|
|
15
15
|
// the defaults (everything off); a MALFORMED file throws, so a typo never silently means "off".
|
|
16
16
|
// `egress:false` or no key makes the effective mode `off`, with the reason.
|
|
17
|
+
// 4. EGRESS IS A TRI-STATE, AND UNANSWERED BEHAVES AS NO (golden-frijoles-plugin D12, S5.4). `egress`
|
|
18
|
+
// is `true | false | null`. The template ships `null`: a stranger's diffs never leave their machine
|
|
19
|
+
// until they say so. `null` and `false` both make the effective mode `off`; `null` ADDITIONALLY
|
|
20
|
+
// emits the D11 ask (`needSetting('jev.egress', { blocking: false })`) once per process, so an agent
|
|
21
|
+
// asks the question instead of the guard silently staying off forever. The fallback reason for `null`
|
|
22
|
+
// is `egress not answered`: `review-guard.mjs` wraps it into the reason `jev could not look (egress not
|
|
23
|
+
// answered)`, and `prose-guard.mjs`'s judgeProse returns it as `why`. Existing consumers keep their committed `egress: true`.
|
|
17
24
|
//
|
|
18
25
|
// Zero deps — Node 18+ (global fetch). Everything with I/O takes injectable deps, so specs never touch the
|
|
19
26
|
// network.
|
|
@@ -22,7 +29,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
22
29
|
import { createHash } from 'node:crypto';
|
|
23
30
|
import { join } from 'node:path';
|
|
24
31
|
import { projectRoot } from './project-root.mjs';
|
|
25
|
-
import { readSection } from './config.mjs';
|
|
32
|
+
import { needSetting, readSection } from './config.mjs';
|
|
26
33
|
|
|
27
34
|
export const JEV_ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
|
|
28
35
|
export const DEFAULT_MODEL = 'jev-1.13.0';
|
|
@@ -75,8 +82,13 @@ export function parseJevConfig(json) {
|
|
|
75
82
|
throw new JevConfigError(
|
|
76
83
|
`jev.config.json: model "${model}" is an alias — pin a version such as ${DEFAULT_MODEL}`
|
|
77
84
|
);
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
// Tri-state (D12): `true | false | null`. An explicit `null` must survive as `null` — `??` would
|
|
86
|
+
// silently turn it back into `true`, which is exactly the "unanswered defaults to send" bug this
|
|
87
|
+
// decision exists to close. Only an ABSENT key falls back to `true` (today's behaviour for a
|
|
88
|
+
// consumer's committed config that never mentions egress at all).
|
|
89
|
+
const egress = Object.prototype.hasOwnProperty.call(json, 'egress') ? json.egress : true;
|
|
90
|
+
if (egress !== true && egress !== false && egress !== null)
|
|
91
|
+
throw new JevConfigError('jev.config.json: egress must be true, false or null');
|
|
80
92
|
if (json.rails !== undefined && !isObj(json.rails))
|
|
81
93
|
throw new JevConfigError('jev.config.json: rails must be an object');
|
|
82
94
|
const rails = {};
|
|
@@ -122,8 +134,16 @@ export function loadJevConfig({ root = repoRoot(), read = readFileSync, exists =
|
|
|
122
134
|
throw new JevConfigError(`jev.config.json: unparseable (${e.message})`);
|
|
123
135
|
},
|
|
124
136
|
});
|
|
125
|
-
// Absent everywhere → the defaults. A PRESENT legacy file holding JSON null is malformed: the
|
|
126
|
-
|
|
137
|
+
// Absent everywhere → the defaults (every rail off). A PRESENT legacy file holding JSON null is malformed: the
|
|
138
|
+
// parser throws.
|
|
139
|
+
if (!present) return parseJevConfig({});
|
|
140
|
+
// D12: a section that never gives egress a non-null value is UNANSWERED, never `true`. readSection treats a
|
|
141
|
+
// `null` in golden-frijoles.config.json as unset, so without this a new-file `egress: null` (or a migrated
|
|
142
|
+
// template config) reached parseJevConfig as a MISSING key and became `true` — sending with nobody's yes
|
|
143
|
+
// (review of the S5 diff). Only parseJevConfig called directly keeps the absent → true fallback.
|
|
144
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw) && (raw.egress === undefined || raw.egress === null))
|
|
145
|
+
return parseJevConfig({ ...raw, egress: null });
|
|
146
|
+
return parseJevConfig(raw);
|
|
127
147
|
}
|
|
128
148
|
|
|
129
149
|
/** Parse `KEY=value` lines. Enough for .env.local; quotes stripped. */
|
|
@@ -164,7 +184,10 @@ export function readApiKey({
|
|
|
164
184
|
export function effectiveMode(config, rail, { key } = {}) {
|
|
165
185
|
const configured = config.rails[rail].mode;
|
|
166
186
|
if (configured === 'off') return { mode: 'off', configured, why: 'configured off' };
|
|
167
|
-
|
|
187
|
+
// Unanswered (D12): distinct from a deliberate `false` so the caller can ask once instead of
|
|
188
|
+
// staying silently off forever. `unanswered: true` is jevContext's cue to fire the D11 protocol.
|
|
189
|
+
if (config.egress === null) return { mode: 'off', configured, why: 'egress not answered', unanswered: true };
|
|
190
|
+
if (!config.egress) return { mode: 'off', configured, why: 'egress disabled (jev.egress: false)' };
|
|
168
191
|
if (!key) return { mode: 'off', configured, why: 'no TYPESAFE_API_KEY' };
|
|
169
192
|
return { mode: configured, configured, why: `configured ${configured}` };
|
|
170
193
|
}
|
|
@@ -325,6 +348,18 @@ export function jevContext(rail, deps = {}) {
|
|
|
325
348
|
const config = deps.config ?? loadJevConfig({ root });
|
|
326
349
|
const key = 'key' in deps ? deps.key : readApiKey({ root });
|
|
327
350
|
const eff = effectiveMode(config, rail, { key });
|
|
351
|
+
// D11's ask protocol, fired exactly once (needSetting's own dedup): egress was never answered, so
|
|
352
|
+
// tell the agent to ask instead of leaving the guard silently off forever. Non-blocking — a judge
|
|
353
|
+
// still returns a decision this run, via the regex fallback the caller already has for `mode:'off'`.
|
|
354
|
+
if (eff.unanswered) {
|
|
355
|
+
(deps.needSetting ?? needSetting)('jev.egress', {
|
|
356
|
+
blocking: false,
|
|
357
|
+
root,
|
|
358
|
+
...(deps.read ? { read: deps.read } : {}),
|
|
359
|
+
...(deps.exists ? { exists: deps.exists } : {}),
|
|
360
|
+
...(deps.write ? { write: deps.write } : {}),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
328
363
|
return {
|
|
329
364
|
root,
|
|
330
365
|
config,
|
package/dist/lib/prose-guard.mjs
CHANGED
|
@@ -707,7 +707,9 @@ export async function judgeProse(draft, evidence = {}, deps = {}) {
|
|
|
707
707
|
const units = proseUnits(draft);
|
|
708
708
|
const questions = proseQuestions(units, evidence);
|
|
709
709
|
if (ctx.mode === 'off' || !questions.length || !String(draft ?? '').trim()) {
|
|
710
|
-
|
|
710
|
+
// `why` names what kept Jev out (`egress not answered`, `no TYPESAFE_API_KEY`, …), the way the review judge's
|
|
711
|
+
// reason does, so a regex-decided draft never reads like the configured path.
|
|
712
|
+
return { ...regex, decider: 'regex', mode: ctx.mode, why: ctx.why, regexCodes, jevCodes: null, fallback: [] };
|
|
711
713
|
}
|
|
712
714
|
|
|
713
715
|
const chunks = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@golden-frijoles/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The scripts the Golden Frijoles skills run, packaged so they work in any repo: planning, build-order, reporting and verification rails. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|