@juzser/blacksmith 0.3.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/.claude/agents/{auditor.md → bs-auditor.md} +8 -5
- package/.claude/agents/{coder.md → bs-coder.md} +8 -7
- package/.claude/agents/{grader.md → bs-grader.md} +16 -9
- package/.claude/agents/{merger.md → bs-merger.md} +4 -4
- package/.claude/agents/{planner.md → bs-planner.md} +20 -6
- package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
- package/.claude/agents/{reviewer.md → bs-reviewer.md} +16 -5
- package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
- package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +9 -6
- package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +18 -10
- package/.claude/agents/{tester.md → bs-tester.md} +40 -12
- package/.claude/agents/bs-uiux.md +257 -0
- package/.claude/agents/{verifier.md → bs-verifier.md} +7 -4
- package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
- package/.claude/skills/bs/SKILL.md +99 -21
- package/.claude/skills/bs/audit.md +45 -31
- package/.claude/skills/bs/dispatch.md +133 -47
- package/.claude/skills/bs/lessons.md +28 -14
- package/.claude/skills/bs/mcp.md +4 -4
- package/.claude/skills/bs/new.md +3 -3
- package/.claude/skills/bs/plan.md +57 -16
- package/.claude/skills/bs/report.md +11 -11
- package/.claude/skills/bs/run.md +190 -52
- package/.claude/skills/bs/status.md +14 -7
- package/.claude/skills/bs/ui.md +21 -5
- package/.claude/skills/bs/waivers.md +78 -2
- package/.claude/skills/bs/wave.md +143 -50
- package/README.md +53 -32
- package/factory/orchestrator/dist/agentNames.js +43 -0
- package/factory/orchestrator/dist/agents-registry.js +11 -4
- package/factory/orchestrator/dist/agentsSync.js +70 -13
- package/factory/orchestrator/dist/artifacts.js +31 -0
- package/factory/orchestrator/dist/attribution.js +4 -0
- package/factory/orchestrator/dist/audit.js +245 -20
- package/factory/orchestrator/dist/autonomy.js +6 -2
- package/factory/orchestrator/dist/budgetAlarm.js +132 -37
- package/factory/orchestrator/dist/budgets.js +302 -84
- package/factory/orchestrator/dist/claims.js +9 -19
- package/factory/orchestrator/dist/cli.js +1087 -116
- package/factory/orchestrator/dist/cliName.js +27 -0
- package/factory/orchestrator/dist/crossFinding.js +20 -0
- package/factory/orchestrator/dist/crosscheck.js +136 -8
- package/factory/orchestrator/dist/daemon.js +352 -67
- package/factory/orchestrator/dist/db/projector.js +327 -54
- package/factory/orchestrator/dist/db/queries.js +1831 -94
- package/factory/orchestrator/dist/db/schema.js +57 -4
- package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
- package/factory/orchestrator/dist/delegation.js +86 -19
- package/factory/orchestrator/dist/diffstat.js +15 -8
- package/factory/orchestrator/dist/dispatchLint.js +108 -0
- package/factory/orchestrator/dist/effort.js +14 -2
- package/factory/orchestrator/dist/effortTiers.js +13 -0
- package/factory/orchestrator/dist/env.js +26 -0
- package/factory/orchestrator/dist/epic.js +494 -42
- package/factory/orchestrator/dist/epicBudget.js +93 -0
- package/factory/orchestrator/dist/epicWidth.js +22 -5
- package/factory/orchestrator/dist/errorIssues.js +182 -11
- package/factory/orchestrator/dist/escalation.js +2 -2
- package/factory/orchestrator/dist/events.js +164 -14
- package/factory/orchestrator/dist/feedback.js +169 -0
- package/factory/orchestrator/dist/findingContext.js +26 -0
- package/factory/orchestrator/dist/findings.js +108 -14
- package/factory/orchestrator/dist/gate.js +120 -10
- package/factory/orchestrator/dist/gh.js +19 -2
- package/factory/orchestrator/dist/git.js +20 -2
- package/factory/orchestrator/dist/goalCheck.js +2 -2
- package/factory/orchestrator/dist/harness.js +25 -17
- package/factory/orchestrator/dist/hookDecision.js +411 -39
- package/factory/orchestrator/dist/integration.js +20 -8
- package/factory/orchestrator/dist/issueReporter.js +81 -16
- package/factory/orchestrator/dist/judgePreflight.js +25 -4
- package/factory/orchestrator/dist/judgeRoles.js +47 -0
- package/factory/orchestrator/dist/judgeStopHook.js +239 -0
- package/factory/orchestrator/dist/judges.js +419 -21
- package/factory/orchestrator/dist/lessons.js +190 -11
- package/factory/orchestrator/dist/logCache.js +291 -0
- package/factory/orchestrator/dist/mcp.js +6 -6
- package/factory/orchestrator/dist/paths.js +20 -7
- package/factory/orchestrator/dist/plan.js +399 -33
- package/factory/orchestrator/dist/planQuorum.js +5 -2
- package/factory/orchestrator/dist/policy.js +1275 -30
- package/factory/orchestrator/dist/projectName.js +25 -0
- package/factory/orchestrator/dist/projects.js +12 -1
- package/factory/orchestrator/dist/promptCapture.js +306 -0
- package/factory/orchestrator/dist/promptHook.js +33 -0
- package/factory/orchestrator/dist/promptImport.js +281 -0
- package/factory/orchestrator/dist/prompts.js +13 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
- package/factory/orchestrator/dist/providers/types.js +1 -1
- package/factory/orchestrator/dist/queue.js +471 -30
- package/factory/orchestrator/dist/quorum.js +18 -5
- package/factory/orchestrator/dist/results.js +25 -7
- package/factory/orchestrator/dist/roadmap.js +10 -5
- package/factory/orchestrator/dist/run-cli.js +15 -10
- package/factory/orchestrator/dist/runner.js +2 -2
- package/factory/orchestrator/dist/scheduler.js +3 -3
- package/factory/orchestrator/dist/schemas.js +56 -8
- package/factory/orchestrator/dist/severity.js +8 -4
- package/factory/orchestrator/dist/spec.js +246 -12
- package/factory/orchestrator/dist/specChange.js +13 -9
- package/factory/orchestrator/dist/stack.js +6 -6
- package/factory/orchestrator/dist/storeRoot.js +90 -0
- package/factory/orchestrator/dist/symbols.js +261 -6
- package/factory/orchestrator/dist/taskEvents.js +192 -31
- package/factory/orchestrator/dist/taskStatus.js +14 -0
- package/factory/orchestrator/dist/taxonomy.js +11 -0
- package/factory/orchestrator/dist/testerAudit.js +31 -12
- package/factory/orchestrator/dist/testgate.js +20 -16
- package/factory/orchestrator/dist/uiuxGate.js +83 -0
- package/factory/orchestrator/dist/usage.js +113 -48
- package/factory/orchestrator/dist/waivers.js +99 -12
- package/factory/orchestrator/dist/waveBudget.js +2 -0
- package/factory/orchestrator/dist/waveConcurrency.js +143 -7
- package/factory/orchestrator/dist/waveSchedule.js +84 -0
- package/factory/orchestrator/dist/workroot.js +1 -1
- package/factory/orchestrator/dist/worktree.js +76 -21
- package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
- package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
- package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
- package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
- package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
- package/factory/policies/budgets.yml +123 -33
- package/factory/policies/crosscheck.yml +51 -13
- package/factory/policies/delegation.yml +3 -3
- package/factory/policies/effort.yml +18 -10
- package/factory/policies/guardrails.yml +8 -6
- package/factory/policies/harness.yml +5 -5
- package/factory/policies/lessons.md +53 -2
- package/factory/policies/scheduler.yml +4 -4
- package/factory/policies/sensitive-paths.yml +1 -1
- package/factory/policies/severity.yml +13 -1
- package/factory/policies/stack.yml +6 -6
- package/factory/policies/taxonomy.yml +9 -3
- package/factory/policies/worktree.yml +15 -9
- package/factory/specs/roadmap.md +27 -3
- package/factory/specs/schema/event.schema.json +8 -1
- package/factory/specs/schema/result.schema.json +21 -8
- package/factory/specs/schema/task-spec.schema.json +12 -0
- package/package.json +16 -12
- package/.claude/agents/uiux.md +0 -133
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bs-rename, operator decision 3: `smith`/`smith-run` keep working as
|
|
3
|
+
* deprecated aliases of `bs`/`bs-run` for a transition period, each printing
|
|
4
|
+
* one deprecation line to stderr (never stdout -- stdout is parsed as JSON by
|
|
5
|
+
* playbooks) when invoked under the old name.
|
|
6
|
+
*
|
|
7
|
+
* Detection is argv[1]'s basename. `npm`/`pnpm`'s `bin` field creates one
|
|
8
|
+
* shim per key, and each is an unextended file (a symlink on POSIX) literally
|
|
9
|
+
* named after that key, sharing the same target script with its non-
|
|
10
|
+
* deprecated sibling -- so this reads back exactly what the operator typed to
|
|
11
|
+
* reach the running process, never the shared script file underneath it.
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
export function invokedName(argv1) {
|
|
15
|
+
return argv1 === undefined ? undefined : path.basename(argv1);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Writes the deprecation line when the current process was invoked as
|
|
19
|
+
* `legacyName`; a no-op for every other invocation, `currentName` included.
|
|
20
|
+
*/
|
|
21
|
+
export function warnIfLegacyName(argv1, legacyName, currentName, writeStderr = (text) => {
|
|
22
|
+
process.stderr.write(text);
|
|
23
|
+
}) {
|
|
24
|
+
if (invokedName(argv1) !== legacyName)
|
|
25
|
+
return;
|
|
26
|
+
writeStderr(`${legacyName}: deprecated, use "${currentName}" instead -- ${legacyName} will be removed in a future release.\n`);
|
|
27
|
+
}
|
|
@@ -123,6 +123,26 @@ export function reconcile(input) {
|
|
|
123
123
|
if (!nativeByFingerprint.has(finding.fingerprint)) {
|
|
124
124
|
nativeByFingerprint.set(finding.fingerprint, finding);
|
|
125
125
|
}
|
|
126
|
+
// Also key by the fingerprint computeFingerprint() would produce for this
|
|
127
|
+
// record TODAY, not just the one stored at raise time. `input.native`
|
|
128
|
+
// comes straight out of listFindings() (a lineage-wide, possibly
|
|
129
|
+
// long-lived read) while `group.fingerprint` below is always computed
|
|
130
|
+
// fresh from the independent provider's evidence -- so a native finding
|
|
131
|
+
// raised before issue #178 widened the normalizer (its summary containing
|
|
132
|
+
// a line span the old algorithm did not strip) would otherwise never
|
|
133
|
+
// match an independent finder's evidence for the identical bug, and get
|
|
134
|
+
// reported as `co-located`/`independent-only` plus a duplicate
|
|
135
|
+
// `native-only` instead of `corroborated`.
|
|
136
|
+
if (finding.file_path !== undefined) {
|
|
137
|
+
const recomputed = computeFingerprint({
|
|
138
|
+
filePath: finding.file_path,
|
|
139
|
+
category: finding.finding_category,
|
|
140
|
+
summary: finding.summary,
|
|
141
|
+
});
|
|
142
|
+
if (!nativeByFingerprint.has(recomputed)) {
|
|
143
|
+
nativeByFingerprint.set(recomputed, finding);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
126
146
|
if (finding.file_path === undefined)
|
|
127
147
|
continue;
|
|
128
148
|
const key = coLocationKey(finding.file_path, finding.finding_category);
|
|
@@ -12,11 +12,27 @@
|
|
|
12
12
|
// backward compatibility with the Phase-1 scaffold (it predates the transport
|
|
13
13
|
// split and no longer carries meaning on its own — `transport` is now the
|
|
14
14
|
// field that actually selects behavior).
|
|
15
|
-
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
16
17
|
import { parse as parseYaml } from 'yaml';
|
|
18
|
+
import { readEnv } from './env.js';
|
|
17
19
|
import { SmithError } from './errors.js';
|
|
18
20
|
import { CROSSCHECK_POLICY_PATH } from './paths.js';
|
|
19
21
|
import { apiKeyPresent, commandOnPath } from './preconditions.js';
|
|
22
|
+
/**
|
|
23
|
+
* OpenRouter (https://openrouter.ai/api/v1) speaks the same OpenAI
|
|
24
|
+
* chat/completions wire format api-transport.ts already sends, so it plays
|
|
25
|
+
* two roles here without a new transport module:
|
|
26
|
+
*
|
|
27
|
+
* - Substitute: a default external provider (codex, deepseek) whose own
|
|
28
|
+
* precondition is unmet on this box runs through OpenRouter instead, using
|
|
29
|
+
* `openrouter_fallback.model` from crosscheck.yml — same provider name,
|
|
30
|
+
* mode and gating power, `via: 'openrouter'` recorded for the log.
|
|
31
|
+
* - Extra judge: `OPENROUTER_MODELS` (comma-separated model ids) synthesizes
|
|
32
|
+
* additional `openrouter:<model>` providers, `mode: shadow` by default.
|
|
33
|
+
*/
|
|
34
|
+
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
35
|
+
const OPENROUTER_API_KEY_ENV = 'OPENROUTER_API_KEY';
|
|
20
36
|
export class CrosscheckError extends SmithError {
|
|
21
37
|
}
|
|
22
38
|
/**
|
|
@@ -246,6 +262,13 @@ function parseProvider(name, raw) {
|
|
|
246
262
|
throw new CrosscheckError('crosscheck.invalid-provider', `Provider "${name}" has transport: cli but no command.`, { provider: name });
|
|
247
263
|
}
|
|
248
264
|
const command = quorumString(`providers.${name}.command`, raw.command);
|
|
265
|
+
// Computed unconditionally (not only under `auto`) so a substitution
|
|
266
|
+
// decision below can ask "can this provider run as declared" even when
|
|
267
|
+
// the file says `enabled: true` outright.
|
|
268
|
+
const preconditionMet = commandOnPath(command);
|
|
269
|
+
if (shouldSubstituteViaOpenrouter(declared, preconditionMet, raw.openrouter_fallback)) {
|
|
270
|
+
return substituteViaOpenrouter(name, mode, modelTier, raw.openrouter_fallback, enabledSource);
|
|
271
|
+
}
|
|
249
272
|
return {
|
|
250
273
|
name,
|
|
251
274
|
kind: 'api',
|
|
@@ -254,7 +277,7 @@ function parseProvider(name, raw) {
|
|
|
254
277
|
// binary runnable here. Not whether it is authenticated -- only a real
|
|
255
278
|
// call knows that, so `auto` can still resolve true on a box where
|
|
256
279
|
// `codex login` was never run, and the quorum records the failure.
|
|
257
|
-
enabled: declared === 'auto' ?
|
|
280
|
+
enabled: declared === 'auto' ? preconditionMet : declared,
|
|
258
281
|
enabledSource,
|
|
259
282
|
mode,
|
|
260
283
|
modelTier,
|
|
@@ -271,13 +294,18 @@ function parseProvider(name, raw) {
|
|
|
271
294
|
throw new CrosscheckError('crosscheck.invalid-provider', `Provider "${name}" has transport: api but is missing base_url/model/api_key_env.`, { provider: name });
|
|
272
295
|
}
|
|
273
296
|
const apiKeyEnv = quorumString(`providers.${name}.api_key_env`, raw.api_key_env);
|
|
297
|
+
// Computed unconditionally for the same reason as the cli branch above.
|
|
298
|
+
const preconditionMet = apiKeyPresent(apiKeyEnv);
|
|
299
|
+
if (shouldSubstituteViaOpenrouter(declared, preconditionMet, raw.openrouter_fallback)) {
|
|
300
|
+
return substituteViaOpenrouter(name, mode, modelTier, raw.openrouter_fallback, enabledSource);
|
|
301
|
+
}
|
|
274
302
|
return {
|
|
275
303
|
name,
|
|
276
304
|
kind: 'api',
|
|
277
305
|
transport: 'api',
|
|
278
306
|
// Set and non-empty, which is all that is knowable without spending a
|
|
279
307
|
// call; whether the key is VALID is not.
|
|
280
|
-
enabled: declared === 'auto' ?
|
|
308
|
+
enabled: declared === 'auto' ? preconditionMet : declared,
|
|
281
309
|
enabledSource,
|
|
282
310
|
mode,
|
|
283
311
|
modelTier,
|
|
@@ -292,6 +320,85 @@ function parseProvider(name, raw) {
|
|
|
292
320
|
}
|
|
293
321
|
throw new CrosscheckError('crosscheck.invalid-provider', `Provider "${name}" is not kind: native and has no recognized transport (got "${String(raw.transport)}").`, { provider: name, transport: raw.transport });
|
|
294
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Operator decision 1a's eligibility rule: a provider substitutes through
|
|
325
|
+
* OpenRouter when it is not explicitly turned off (`enabled: false` stays an
|
|
326
|
+
* opt-out, whether the reason is cost, policy, or "I don't want a
|
|
327
|
+
* substitute"), its own precondition failed on this box, the policy names a
|
|
328
|
+
* fallback model for it, and OPENROUTER_API_KEY is actually set here.
|
|
329
|
+
*/
|
|
330
|
+
function shouldSubstituteViaOpenrouter(declared, preconditionMet, fallback) {
|
|
331
|
+
return (declared !== false &&
|
|
332
|
+
!preconditionMet &&
|
|
333
|
+
Boolean(fallback) &&
|
|
334
|
+
apiKeyPresent(OPENROUTER_API_KEY_ENV));
|
|
335
|
+
}
|
|
336
|
+
function substituteViaOpenrouter(name, mode, modelTier, fallback, enabledSource) {
|
|
337
|
+
const model = quorumString(`providers.${name}.openrouter_fallback.model`, fallback.model);
|
|
338
|
+
return {
|
|
339
|
+
name,
|
|
340
|
+
kind: 'api',
|
|
341
|
+
transport: 'api',
|
|
342
|
+
enabled: true,
|
|
343
|
+
enabledSource,
|
|
344
|
+
mode,
|
|
345
|
+
modelTier,
|
|
346
|
+
baseUrl: OPENROUTER_BASE_URL,
|
|
347
|
+
model,
|
|
348
|
+
apiKeyEnv: OPENROUTER_API_KEY_ENV,
|
|
349
|
+
responseFormatJsonObject: fallback.response_format_json_object ?? true,
|
|
350
|
+
via: 'openrouter',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* `OPENROUTER_MODELS` (comma-separated OpenRouter model ids) synthesizes
|
|
355
|
+
* extra judges, operator decision 1b: `openrouter:<model>` as the provider
|
|
356
|
+
* name (deterministic, so a repeated run of the same env names the same
|
|
357
|
+
* provider), `mode: shadow` unless a project/policy overlay promotes it.
|
|
358
|
+
*/
|
|
359
|
+
function envOpenrouterProviders() {
|
|
360
|
+
const raw = (process.env.OPENROUTER_MODELS ?? '').trim();
|
|
361
|
+
if (!raw)
|
|
362
|
+
return {};
|
|
363
|
+
const providers = {};
|
|
364
|
+
for (const model of raw
|
|
365
|
+
.split(',')
|
|
366
|
+
.map((entry) => entry.trim())
|
|
367
|
+
.filter(Boolean)) {
|
|
368
|
+
providers[`openrouter:${model}`] = {
|
|
369
|
+
kind: 'api',
|
|
370
|
+
transport: 'api',
|
|
371
|
+
enabled: 'auto',
|
|
372
|
+
mode: 'shadow',
|
|
373
|
+
model_tier: DEFAULT_MODEL_TIER,
|
|
374
|
+
base_url: OPENROUTER_BASE_URL,
|
|
375
|
+
model,
|
|
376
|
+
api_key_env: OPENROUTER_API_KEY_ENV,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return providers;
|
|
380
|
+
}
|
|
381
|
+
/** `<project-dir>/.blacksmith/crosscheck.yml` — mirrors auditStorePath()'s STORE_DIR pattern in audit.ts. */
|
|
382
|
+
export function crosscheckOverlayPath(projectDir) {
|
|
383
|
+
return path.join(path.resolve(projectDir), '.blacksmith', 'crosscheck.yml');
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The per-project overlay (config source 1, highest precedence): only a
|
|
387
|
+
* `providers:` map is legal here, merged by provider name over env and the
|
|
388
|
+
* shipped policy. Quorum rules and everything else stay repo policy, so any
|
|
389
|
+
* other top-level key is refused loudly rather than silently ignored.
|
|
390
|
+
*/
|
|
391
|
+
export function loadProjectProviderOverlay(projectDir) {
|
|
392
|
+
const filePath = crosscheckOverlayPath(projectDir);
|
|
393
|
+
if (!existsSync(filePath))
|
|
394
|
+
return {};
|
|
395
|
+
const doc = (parseYaml(readFileSync(filePath, 'utf8')) ?? {});
|
|
396
|
+
const extraKeys = Object.keys(doc).filter((key) => key !== 'providers');
|
|
397
|
+
if (extraKeys.length > 0) {
|
|
398
|
+
throw new CrosscheckError('crosscheck.invalid-overlay', `${filePath} may only set "providers" -- rejected ${extraKeys.map((key) => JSON.stringify(key)).join(', ')}. Quorum rules and every other crosscheck.yml knob stay repo policy; only which providers run is a per-project decision.`, { file: filePath, keys: extraKeys });
|
|
399
|
+
}
|
|
400
|
+
return doc.providers ?? {};
|
|
401
|
+
}
|
|
295
402
|
/**
|
|
296
403
|
* The isolation pair list, rebuilt entry by entry for quorumPairs()' reason:
|
|
297
404
|
* a half-readable list is the same hole as no list. An empty list stays legal
|
|
@@ -359,9 +466,27 @@ export function parseCrosscheckPolicy(yamlText, options = {}) {
|
|
|
359
466
|
if (!doc.providers || Object.keys(doc.providers).length === 0) {
|
|
360
467
|
throw new CrosscheckError('crosscheck.invalid-policy', 'crosscheck.yml has no providers.');
|
|
361
468
|
}
|
|
469
|
+
const declaredProviders = doc.providers;
|
|
470
|
+
const envProviders = envOpenrouterProviders();
|
|
471
|
+
const overlay = options.providerOverlay ?? {};
|
|
472
|
+
const names = new Set([
|
|
473
|
+
...Object.keys(declaredProviders),
|
|
474
|
+
...Object.keys(envProviders),
|
|
475
|
+
...Object.keys(overlay),
|
|
476
|
+
]);
|
|
362
477
|
const providers = {};
|
|
363
|
-
for (const
|
|
364
|
-
const
|
|
478
|
+
for (const name of names) {
|
|
479
|
+
const fromPolicy = declaredProviders[name];
|
|
480
|
+
const fromEnv = envProviders[name];
|
|
481
|
+
const fromOverlay = overlay[name];
|
|
482
|
+
const merged = {
|
|
483
|
+
...(fromPolicy ?? {}),
|
|
484
|
+
...(fromEnv ?? {}),
|
|
485
|
+
...(fromOverlay ?? {}),
|
|
486
|
+
};
|
|
487
|
+
const configSource = fromOverlay ? 'project' : fromEnv ? 'env' : 'policy';
|
|
488
|
+
const parsed = parseProvider(name, merged);
|
|
489
|
+
const config = parsed.kind === 'native' ? parsed : { ...parsed, configSource };
|
|
365
490
|
providers[name] =
|
|
366
491
|
options.offline && config.kind !== 'native'
|
|
367
492
|
? // The switch is the decider now, and says so: a reader that saw
|
|
@@ -391,8 +516,9 @@ export function parseCrosscheckPolicy(yamlText, options = {}) {
|
|
|
391
516
|
};
|
|
392
517
|
}
|
|
393
518
|
/**
|
|
394
|
-
* Reads the policy, honouring `
|
|
395
|
-
*
|
|
519
|
+
* Reads the policy, honouring `BS_CROSSCHECK_OFFLINE` (or its legacy
|
|
520
|
+
* `SMITH_CROSSCHECK_OFFLINE` fallback): set it to anything non-empty and no
|
|
521
|
+
* external provider can be invoked, whatever the file says.
|
|
396
522
|
*
|
|
397
523
|
* The switch is read here rather than passed down from each call site because
|
|
398
524
|
* the property it protects is process-wide — "this process makes no judge
|
|
@@ -401,7 +527,9 @@ export function parseCrosscheckPolicy(yamlText, options = {}) {
|
|
|
401
527
|
* built binary as a subprocess; nothing else would reach them.
|
|
402
528
|
*/
|
|
403
529
|
export function loadCrosscheckPolicy(filePath = CROSSCHECK_POLICY_PATH, options = {}) {
|
|
530
|
+
const { projectDir, ...rest } = options;
|
|
404
531
|
return parseCrosscheckPolicy(readFileSync(filePath, 'utf8'), {
|
|
405
|
-
offline:
|
|
532
|
+
offline: rest.offline ?? Boolean(readEnv(process.env, 'SMITH_CROSSCHECK_OFFLINE')),
|
|
533
|
+
providerOverlay: projectDir ? loadProjectProviderOverlay(projectDir) : rest.providerOverlay,
|
|
406
534
|
});
|
|
407
535
|
}
|