@sensigo/realm-cli 0.41.0 → 0.43.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/agent/run-attach.d.ts.map +1 -1
- package/dist/agent/run-attach.js +8 -21
- package/dist/agent/run-attach.js.map +1 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +23 -6
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +6 -5
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +4 -2
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/register.d.ts +1 -39
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +21 -89
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/replay.d.ts.map +1 -1
- package/dist/commands/replay.js +4 -2
- package/dist/commands/replay.js.map +1 -1
- package/dist/commands/respond.d.ts.map +1 -1
- package/dist/commands/respond.js +8 -3
- package/dist/commands/respond.js.map +1 -1
- package/dist/commands/resume.d.ts.map +1 -1
- package/dist/commands/resume.js +3 -2
- package/dist/commands/resume.js.map +1 -1
- package/dist/commands/run.d.ts +4 -3
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +30 -10
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +8 -5
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/validate.d.ts +12 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +444 -170
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/watch.d.ts +14 -2
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +269 -72
- package/dist/commands/watch.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/admission-context.d.ts +58 -0
- package/dist/lib/admission-context.d.ts.map +1 -0
- package/dist/lib/admission-context.js +57 -0
- package/dist/lib/admission-context.js.map +1 -0
- package/dist/lib/load-workflow-for-admission.d.ts +69 -0
- package/dist/lib/load-workflow-for-admission.d.ts.map +1 -0
- package/dist/lib/load-workflow-for-admission.js +130 -0
- package/dist/lib/load-workflow-for-admission.js.map +1 -0
- package/dist/lib/loader-warnings.d.ts +26 -9
- package/dist/lib/loader-warnings.d.ts.map +1 -1
- package/dist/lib/loader-warnings.js +51 -31
- package/dist/lib/loader-warnings.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
// realm validate <path> — validates a workflow YAML file without registering it.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// ONE admission path (issue #553): `validate <file>` calls the same `loadWorkflowForAdmission`
|
|
4
|
+
// register and watch call — file loader (agent-profile resolution), the unconditional
|
|
5
|
+
// project-extensions pass (modules, manifest, config_schema two-pass), real-then-sentinel
|
|
6
|
+
// secret resolution — so what validate blesses register accepts, and what register refuses
|
|
7
|
+
// validate refuses, by construction. The pre-#553 "strictness asymmetry" (extension-free
|
|
8
|
+
// workflows parsed from string, skipping every file-context check) is gone; `--registered`
|
|
9
|
+
// runs the same rules on the stored copy and supplies-or-declares the context-dependent ones.
|
|
8
10
|
import { Command } from 'commander';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { loadWorkflowFromStringWithDiagnostics, resolveAgentProfiles, WorkflowError, shouldEnforceTimeout, DEFAULT_EXECUTION_TIMEOUT_SECONDS, resolveSeverity, renderLoaderWarning, assessStructuredOutputEligibility, renderIneligibleMessage, JsonWorkflowStore, RUNTIME_ONLY_WORKFLOW_KEYS, VERSION, } from '@sensigo/realm';
|
|
14
|
+
import { renderLoadFailure, renderEscalationLine, printLoaderWarnings, rejectOnErrorSeverity, failsStrict, wrapSentinelWarnings, extensionKeysOf, renderExtensionKeysClause, } from '../lib/loader-warnings.js';
|
|
15
|
+
import { loadWorkflowForAdmission, admitProjectExtensions, ExtensionLoadError, admittedDefinitionOf, } from '../lib/load-workflow-for-admission.js';
|
|
16
|
+
import { CONTEXT_DEPENDENT_CHECKS, notRunReason, renderChecksNotRunLine, } from '../lib/admission-context.js';
|
|
15
17
|
/**
|
|
16
18
|
* Advisory (issue A3, never rejects): an auto step declaring `retry:` but no `timeout_seconds`
|
|
17
19
|
* has EVERY attempt bounded by the generous DEFAULT_EXECUTION_TIMEOUT_SECONDS default — a hung
|
|
@@ -56,20 +58,42 @@ export function findRetryWithoutExplicitTimeout(definition) {
|
|
|
56
58
|
* accumulator turns the summary line into a failing one and returns true (the caller exits 1);
|
|
57
59
|
* otherwise the summary line — and, when present, the description line existing tests assert on
|
|
58
60
|
* — print exactly as before and this returns false.
|
|
61
|
+
*
|
|
62
|
+
* `checksNotRun` (issue #553 correction C9, default 0 — the file-mode call site never passes it,
|
|
63
|
+
* since every check runs there) adds the FIRST tail clause when non-zero: `N check(s) not run`.
|
|
64
|
+
* There is no non-strict `— N warning(s)` tail today and this does not invent one — a
|
|
65
|
+
* warnings-bearing run without `--strict` and without a not-run count still prints the bare line,
|
|
66
|
+
* warnings above it. The failing-`--strict` clause, when both fire, comes SECOND, `; `-joined
|
|
67
|
+
* with the not-run clause — `— N check(s) not run; M warning(s); failing due to --strict` — and
|
|
68
|
+
* the description-suppression rule is UNCHANGED: only a failing `--strict` suppresses it, not a
|
|
69
|
+
* bare not-run disclosure (a moved tree is not a reason to hide the workflow's own description).
|
|
70
|
+
*
|
|
71
|
+
* `extensionKeys` (issue #559, default `[]`) adds a SECOND tail clause, between the not-run
|
|
72
|
+
* clause and the strict clause, naming every top-level `x-` key the definition carries — the
|
|
73
|
+
* accepted-but-unread namespace mints no warning of its own, so this is the only disclosure an
|
|
74
|
+
* author who believed `x-timeout` configured something gets.
|
|
59
75
|
*/
|
|
60
|
-
function printValidationOutcome(definition, warnings, strict) {
|
|
76
|
+
function printValidationOutcome(definition, warnings, strict, checksNotRun = 0, extensionKeys = []) {
|
|
61
77
|
printLoaderWarnings(warnings);
|
|
62
78
|
const stepCount = Object.keys(definition.steps).length;
|
|
63
79
|
const base = `Valid: ${definition.id} v${definition.version} (${stepCount} ${stepCount === 1 ? 'step' : 'steps'})`;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
const strictFailing = strict && failsStrict(warnings);
|
|
81
|
+
const clauses = [];
|
|
82
|
+
if (checksNotRun > 0) {
|
|
83
|
+
clauses.push(`${checksNotRun} ${checksNotRun === 1 ? 'check' : 'checks'} not run`);
|
|
84
|
+
}
|
|
85
|
+
const extensionClause = renderExtensionKeysClause(extensionKeys);
|
|
86
|
+
if (extensionClause !== undefined) {
|
|
87
|
+
clauses.push(extensionClause);
|
|
67
88
|
}
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
if (strictFailing) {
|
|
90
|
+
clauses.push(`${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}; failing due to --strict`);
|
|
91
|
+
}
|
|
92
|
+
console.log(clauses.length > 0 ? `${base} — ${clauses.join('; ')}` : base);
|
|
93
|
+
if (!strictFailing && definition.description !== undefined) {
|
|
70
94
|
console.log(` ${definition.description}`);
|
|
71
95
|
}
|
|
72
|
-
return
|
|
96
|
+
return strictFailing;
|
|
73
97
|
}
|
|
74
98
|
/** issue #236: the reasoning-position heuristic (design record §7, ratified via fixture C6) —
|
|
75
99
|
* top-level property NAME match only, never a value/content inspection. */
|
|
@@ -105,6 +129,9 @@ function findReasoningLikeTopLevelProperty(schema) {
|
|
|
105
129
|
*
|
|
106
130
|
* Never printed for an ineligible-and-opted-in step: the LOADER already rejected that combination
|
|
107
131
|
* at load time, so this function structurally never observes it.
|
|
132
|
+
*
|
|
133
|
+
* issue #454: this whole channel is suppressed under `--json` — a caller checking
|
|
134
|
+
* `opts.json` never calls this at all; there is no machinery for it here.
|
|
108
135
|
*/
|
|
109
136
|
function printStructuredOutputNudge(definition, opts) {
|
|
110
137
|
// Not-opted-in steps whose detail either renders (--explain) or feeds the summary counts.
|
|
@@ -214,6 +241,50 @@ function renderNudgeSummary(ready, withCaveats, oneAway) {
|
|
|
214
241
|
line += `, ${oneAway} ${steps(oneAway)} one change away`;
|
|
215
242
|
return line + tail;
|
|
216
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* issue #454 — the severity `--json` reports for every diagnostic. Every mint site in the tree
|
|
246
|
+
* ALREADY resolves severity at construction (`severity: resolveSeverity(code)`,
|
|
247
|
+
* diagnostics.ts:239 and its siblings, under DEFAULT_POLICY — never the `--strict` all-error
|
|
248
|
+
* policy, which is a run mode reported separately in the `strict` block) — so minted ≡ effective
|
|
249
|
+
* for every constructible diagnostic today, and this re-resolution is a GUARD against a future
|
|
250
|
+
* mint or a policy change landing without updating this file, not a live divergence. No real
|
|
251
|
+
* fixture can distinguish the two; only a hand-constructed lying warning can (validate-json.test.ts's
|
|
252
|
+
* U1 cell).
|
|
253
|
+
* @internal Exported for testing only.
|
|
254
|
+
*/
|
|
255
|
+
export function normalizeDiagnosticSeverity(w) {
|
|
256
|
+
return { ...w, severity: resolveSeverity(w.code) };
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The ONE `--json` emission point (issue #454): every contract arm builds a `ValidateJsonEmit`
|
|
260
|
+
* and calls this, so the machine channel cannot drift arm-to-arm the way independently-written
|
|
261
|
+
* `JSON.stringify` call sites could. One `console.log` carrying the whole object is itself the
|
|
262
|
+
* purity guarantee this surface's cells assert on (nothing else may write to stdout on a
|
|
263
|
+
* contract arm) — `JSON.stringify(obj, null, 2)`, the `workflow list --json` sibling's own idiom.
|
|
264
|
+
*/
|
|
265
|
+
function emitValidateJson(result) {
|
|
266
|
+
console.log(JSON.stringify({
|
|
267
|
+
valid: result.valid,
|
|
268
|
+
mode: result.mode,
|
|
269
|
+
path: result.path,
|
|
270
|
+
workflow_id: result.workflowId,
|
|
271
|
+
loader_version: VERSION,
|
|
272
|
+
schema_version: result.schemaVersion,
|
|
273
|
+
error_count: result.errors.length,
|
|
274
|
+
warning_count: result.diagnostics.length,
|
|
275
|
+
strict: { requested: result.strictRequested, failed: result.strictFailed },
|
|
276
|
+
diagnostics: result.diagnostics.map(normalizeDiagnosticSeverity),
|
|
277
|
+
errors: result.errors,
|
|
278
|
+
checks_not_run: result.checksNotRun,
|
|
279
|
+
extension_keys: result.extensionKeys,
|
|
280
|
+
}, null, 2));
|
|
281
|
+
}
|
|
282
|
+
/** issue #454 — the shared `err.warnings ?? []` shape four `--json` load-failure sites need
|
|
283
|
+
* (:527, :580, :621, :423 in the pre-#454 line numbering) — everywhere EXCEPT the orphan-guard
|
|
284
|
+
* site (:548), whose diagnostics are the human loop's own accumulated set, not `err.warnings`. */
|
|
285
|
+
function warningsOf(err) {
|
|
286
|
+
return err instanceof WorkflowError ? (err.warnings ?? []) : [];
|
|
287
|
+
}
|
|
217
288
|
/**
|
|
218
289
|
* The ONE place a load failure is rendered on this command (issue #445).
|
|
219
290
|
*
|
|
@@ -230,9 +301,30 @@ function renderNudgeSummary(ready, withCaveats, oneAway) {
|
|
|
230
301
|
* SCOPE, deliberate: watch/register/test/agent keep their own catches (#425's recorded
|
|
231
302
|
* exclusion). If a second surface ever adopts this, move it to lib/loader-warnings.ts — one
|
|
232
303
|
* caller does not earn a shared home.
|
|
304
|
+
*
|
|
305
|
+
* issue #454 — `jsonCtx`, when present, means `--json` was requested: the `errors[]` convention
|
|
306
|
+
* there is the RAW `err.errors ?? [err.message]` (channel prefixes like `Error: `/`Invalid: `
|
|
307
|
+
* are print-time decoration this never applied in the first place — nothing to strip), never the
|
|
308
|
+
* human-rendered `renderLoadFailure(err)` string.
|
|
233
309
|
*/
|
|
234
|
-
function exitOnLoadFailure(err) {
|
|
310
|
+
function exitOnLoadFailure(err, jsonCtx) {
|
|
235
311
|
if (err instanceof WorkflowError) {
|
|
312
|
+
if (jsonCtx !== undefined) {
|
|
313
|
+
emitValidateJson({
|
|
314
|
+
valid: false,
|
|
315
|
+
mode: jsonCtx.mode,
|
|
316
|
+
path: jsonCtx.path,
|
|
317
|
+
workflowId: jsonCtx.workflowId,
|
|
318
|
+
schemaVersion: jsonCtx.schemaVersion,
|
|
319
|
+
strictRequested: jsonCtx.strictRequested,
|
|
320
|
+
strictFailed: false,
|
|
321
|
+
diagnostics: jsonCtx.diagnostics,
|
|
322
|
+
errors: err.errors ?? [err.message],
|
|
323
|
+
checksNotRun: jsonCtx.checksNotRun,
|
|
324
|
+
extensionKeys: [],
|
|
325
|
+
});
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
236
328
|
if (err.warnings !== undefined)
|
|
237
329
|
printLoaderWarnings(err.warnings);
|
|
238
330
|
console.error(renderLoadFailure(err));
|
|
@@ -256,25 +348,12 @@ function rejectIfPolicyEscalates(warnings) {
|
|
|
256
348
|
//
|
|
257
349
|
// register and watch print the same line since issue #451 (watch adds its timestamp and a
|
|
258
350
|
// `— refusing to register.` tail, because it does not exit); it lives in lib/loader-warnings.ts
|
|
259
|
-
// for that reason. On all three surfaces
|
|
260
|
-
//
|
|
351
|
+
// for that reason. On all three surfaces the warnings printed one line above still read
|
|
352
|
+
// `— ignored` (issue #540 deleted the CLI's print-time rewrite to `— REFUSED below`) — this
|
|
353
|
+
// line is where the refusal, and WHICH warning triggered it, actually gets said.
|
|
261
354
|
console.error(renderEscalationLine(warnings));
|
|
262
355
|
return true;
|
|
263
356
|
}
|
|
264
|
-
/** Pre-scan: does the YAML carry a top-level `extensions` key? (Parse errors → false; the
|
|
265
|
-
* real loader below reports them with its existing error surface.) */
|
|
266
|
-
function hasTopLevelExtensions(content) {
|
|
267
|
-
try {
|
|
268
|
-
const raw = load(content);
|
|
269
|
-
return (typeof raw === 'object' &&
|
|
270
|
-
raw !== null &&
|
|
271
|
-
!Array.isArray(raw) &&
|
|
272
|
-
'extensions' in raw);
|
|
273
|
-
}
|
|
274
|
-
catch {
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
357
|
/**
|
|
279
358
|
* `validate --registered <id>` — audit the STORED copy of a workflow (issue #427).
|
|
280
359
|
*
|
|
@@ -289,7 +368,7 @@ function hasTopLevelExtensions(content) {
|
|
|
289
368
|
* change what your runs do. A legacy (schema_version-less or older) copy is a different case
|
|
290
369
|
* entirely — see the legacy arm below: it is not grandfathered, it is already unreachable.
|
|
291
370
|
*/
|
|
292
|
-
async function validateRegistered(id, strict) {
|
|
371
|
+
async function validateRegistered(id, strict, json, overrideModule) {
|
|
293
372
|
const store = new JsonWorkflowStore();
|
|
294
373
|
let stored;
|
|
295
374
|
try {
|
|
@@ -297,6 +376,22 @@ async function validateRegistered(id, strict) {
|
|
|
297
376
|
}
|
|
298
377
|
catch (err) {
|
|
299
378
|
if (err instanceof WorkflowError && err.code === 'STATE_WORKFLOW_NOT_FOUND') {
|
|
379
|
+
if (json) {
|
|
380
|
+
emitValidateJson({
|
|
381
|
+
valid: false,
|
|
382
|
+
mode: 'registered',
|
|
383
|
+
path: null,
|
|
384
|
+
workflowId: id,
|
|
385
|
+
schemaVersion: null,
|
|
386
|
+
strictRequested: strict,
|
|
387
|
+
strictFailed: false,
|
|
388
|
+
diagnostics: [],
|
|
389
|
+
errors: [err.message],
|
|
390
|
+
checksNotRun: [],
|
|
391
|
+
extensionKeys: [],
|
|
392
|
+
});
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
300
395
|
console.error(`Error: ${err.message}`);
|
|
301
396
|
console.error('Registered workflows: realm workflow list');
|
|
302
397
|
process.exit(1);
|
|
@@ -307,6 +402,22 @@ async function validateRegistered(id, strict) {
|
|
|
307
402
|
// consumer resolves through this same get() gate (start_run, execute_step, append_trace,
|
|
308
403
|
// get_workflow_protocol, submit_human_response, replay), so a legacy entry cannot run at
|
|
309
404
|
// all. It is not grandfathered; it is unreachable.
|
|
405
|
+
if (json) {
|
|
406
|
+
emitValidateJson({
|
|
407
|
+
valid: false,
|
|
408
|
+
mode: 'registered',
|
|
409
|
+
path: null,
|
|
410
|
+
workflowId: id,
|
|
411
|
+
schemaVersion: null,
|
|
412
|
+
strictRequested: strict,
|
|
413
|
+
strictFailed: false,
|
|
414
|
+
diagnostics: [],
|
|
415
|
+
errors: [err.message],
|
|
416
|
+
checksNotRun: [],
|
|
417
|
+
extensionKeys: [],
|
|
418
|
+
});
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
310
421
|
console.log(`Auditing the registered copy of '${id}' with realm ${VERSION}'s loader.`);
|
|
311
422
|
// The store's own message carries the remedy; the loader would say `Missing required
|
|
312
423
|
// field: 'steps'` here, which is true of the shape and useless about the cause.
|
|
@@ -316,44 +427,216 @@ async function validateRegistered(id, strict) {
|
|
|
316
427
|
if (!(err instanceof WorkflowError)) {
|
|
317
428
|
// get()'s try wraps ONLY the read — JSON.parse sits outside it, so a corrupt stored file
|
|
318
429
|
// arrives here as a bare SyntaxError with no code (executed).
|
|
319
|
-
|
|
430
|
+
const notParseableMsg = `the registered copy of '${id}' is not parseable JSON: ${err instanceof Error ? err.message : String(err)}`;
|
|
431
|
+
if (json) {
|
|
432
|
+
emitValidateJson({
|
|
433
|
+
valid: false,
|
|
434
|
+
mode: 'registered',
|
|
435
|
+
path: null,
|
|
436
|
+
workflowId: id,
|
|
437
|
+
schemaVersion: null,
|
|
438
|
+
strictRequested: strict,
|
|
439
|
+
strictFailed: false,
|
|
440
|
+
diagnostics: [],
|
|
441
|
+
errors: [notParseableMsg],
|
|
442
|
+
checksNotRun: [],
|
|
443
|
+
extensionKeys: [],
|
|
444
|
+
});
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
console.error(`Error: ${notParseableMsg}`);
|
|
320
448
|
console.error('Registered workflows: realm workflow list');
|
|
321
449
|
process.exit(1);
|
|
322
450
|
}
|
|
323
451
|
throw err; // #123: an unexpected WorkflowError is a bug, and bugs stay loud.
|
|
324
452
|
}
|
|
325
|
-
|
|
326
|
-
`
|
|
327
|
-
|
|
453
|
+
if (!json) {
|
|
454
|
+
console.log(`Auditing the registered copy of '${id}' (schema_version ${String(stored.schema_version)}) ` +
|
|
455
|
+
`with realm ${VERSION}'s loader.`);
|
|
456
|
+
console.log('Registered copies stay grandfathered at runtime against LOADER changes — this reports ' +
|
|
457
|
+
'what re-registration today would say. A NEW engine-side dispatch check (issue #508, ' +
|
|
458
|
+
'realm 0.42.0) is NOT grandfathered: it applies immediately, whatever schema_version ' +
|
|
459
|
+
'is on file.');
|
|
460
|
+
}
|
|
461
|
+
// issue #553 — read the recorded context BEFORE the strip: these are the paths the
|
|
462
|
+
// context-dependent checks are supplied with (the file loader stamps both since v0.14;
|
|
463
|
+
// older copies carry neither, and say so below).
|
|
464
|
+
const recorded = {
|
|
465
|
+
source_dir: typeof stored.source_dir === 'string' ? stored.source_dir : undefined,
|
|
466
|
+
trust_root: typeof stored.trust_root === 'string' ? stored.trust_root : undefined,
|
|
467
|
+
};
|
|
468
|
+
const storedExtensions = stored.extensions;
|
|
328
469
|
const clone = { ...stored };
|
|
329
470
|
for (const key of RUNTIME_ONLY_WORKFLOW_KEYS)
|
|
330
471
|
delete clone[key];
|
|
331
|
-
const declaresProfile = Object.values(stored.steps ?? {}).some((step) => step.agent_profile !== undefined);
|
|
332
|
-
if (clone['extensions'] !== undefined || declaresProfile) {
|
|
333
|
-
console.log('Extensions/profiles declared — module resolution, config_schema checks, and agent-profile ' +
|
|
334
|
-
'file resolution need the source tree and are not audited here; structural rules only.');
|
|
335
|
-
}
|
|
336
472
|
// MUST delete: the from-string loader hard-throws on an `extensions` key (allowExtensions:
|
|
337
473
|
// false) with "Register this workflow from its YAML file" — maximally misleading here, where
|
|
338
|
-
// the workflow IS registered and the operator asked about the stored copy. The
|
|
339
|
-
//
|
|
474
|
+
// the workflow IS registered and the operator asked about the stored copy. The declared
|
|
475
|
+
// modules are re-attached to the parsed definition below, for the extensions pass that
|
|
476
|
+
// actually consumes them.
|
|
340
477
|
delete clone['extensions'];
|
|
478
|
+
const stripped = JSON.stringify(clone);
|
|
479
|
+
// Supply or declare (issue #553): per member of CONTEXT_DEPENDENT_CHECKS, either the recorded
|
|
480
|
+
// path still exists and the check RUNS with it, or the check is declared not run — here, on
|
|
481
|
+
// the human line, and in `checks_not_run` on every `--json` arm from this point.
|
|
482
|
+
const notRun = [];
|
|
483
|
+
const registeredCtx = (workflowId, diagnostics) => ({
|
|
484
|
+
mode: 'registered',
|
|
485
|
+
path: null,
|
|
486
|
+
workflowId,
|
|
487
|
+
schemaVersion: stored.schema_version ?? null,
|
|
488
|
+
diagnostics,
|
|
489
|
+
strictRequested: strict,
|
|
490
|
+
checksNotRun: notRun,
|
|
491
|
+
});
|
|
341
492
|
let definition;
|
|
342
493
|
let loaderWarnings;
|
|
343
494
|
try {
|
|
344
|
-
|
|
495
|
+
// After #553 the four context-free rules (context_wrapper, the workflow_context names,
|
|
496
|
+
// source.path) run here too — the string loader carries them. No `(line N)`: the body is
|
|
497
|
+
// JSON, not the author's file.
|
|
498
|
+
({ definition, warnings: loaderWarnings } = loadWorkflowFromStringWithDiagnostics(stripped));
|
|
345
499
|
}
|
|
346
500
|
catch (err) {
|
|
347
|
-
exitOnLoadFailure(err);
|
|
501
|
+
exitOnLoadFailure(err, json ? registeredCtx(id, warningsOf(err)) : undefined);
|
|
502
|
+
}
|
|
503
|
+
const applicable = CONTEXT_DEPENDENT_CHECKS.filter((check) => check.applies(definition));
|
|
504
|
+
for (const check of applicable) {
|
|
505
|
+
const path = recorded[check.needs];
|
|
506
|
+
// `existsSync` is load-bearing for BOTH members: `resolveAgentProfiles` against a dead
|
|
507
|
+
// tree would name a path under the missing tree (the WRONG error), and
|
|
508
|
+
// `admitProjectExtensions` against a nonexistent trust root returns defaults SILENTLY
|
|
509
|
+
// (executed) — a copy whose tree moved would audit as if it had no manifest at all.
|
|
510
|
+
if (path === undefined || !existsSync(path)) {
|
|
511
|
+
const reason = notRunReason(check.needs, path, stored.origin);
|
|
512
|
+
// issue #553 correction C5 — the extensions member cannot apply an override it never
|
|
513
|
+
// reaches: say so beside the reason, never silently, on the human line AND
|
|
514
|
+
// `checks_not_run[].reason` alike, so 7c's label+reason parity holds by construction.
|
|
515
|
+
notRun.push({
|
|
516
|
+
id: check.id,
|
|
517
|
+
reason: check.id === 'project_extensions' && overrideModule !== undefined
|
|
518
|
+
? `${reason}; --extensions-module not applied`
|
|
519
|
+
: reason,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// The disclosure line — always on, never verbose-gated, the old honesty line's slot: after
|
|
524
|
+
// the header, before any verdict. Derived from the constant; nothing hand-typed. A non-empty
|
|
525
|
+
// set does NOT flip `--strict`: a disclosure, not a warning — a moved tree must not fail CI
|
|
526
|
+
// for a reason the operator cannot act on.
|
|
527
|
+
if (!json && notRun.length > 0)
|
|
528
|
+
console.log(renderChecksNotRunLine(notRun));
|
|
529
|
+
let sentinelWarnings;
|
|
530
|
+
for (const check of applicable) {
|
|
531
|
+
if (notRun.some((n) => n.id === check.id))
|
|
532
|
+
continue;
|
|
533
|
+
if (check.id === 'agent_profile_resolution') {
|
|
534
|
+
try {
|
|
535
|
+
resolveAgentProfiles(definition, recorded.source_dir);
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
exitOnLoadFailure(err, json ? registeredCtx(definition.id, loaderWarnings) : undefined);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
// The extensions pass exactly as loadWorkflowForAdmission runs it — ONE shared call
|
|
543
|
+
// (issue #553 correction C2: `admitProjectExtensions` is now the single mint site for the
|
|
544
|
+
// sentinel-credentials advisory pair, replacing this arm's own hand-typed copy) — against
|
|
545
|
+
// the RECORDED paths, re-stamped on the parsed copy (the strip removed them).
|
|
546
|
+
if (recorded.source_dir !== undefined)
|
|
547
|
+
definition.source_dir = recorded.source_dir;
|
|
548
|
+
definition.trust_root = recorded.trust_root; // the member ran ⇒ recorded and present
|
|
549
|
+
if (storedExtensions !== undefined)
|
|
550
|
+
definition.extensions = storedExtensions;
|
|
551
|
+
let loaded;
|
|
552
|
+
try {
|
|
553
|
+
loaded = await admitProjectExtensions(definition, {
|
|
554
|
+
surface: 'validate',
|
|
555
|
+
...(overrideModule !== undefined ? { overrideModule } : {}),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
// issue #445's sentence, issue #454's whole-message convention, issue #463's
|
|
560
|
+
// warnings-first — the file arm's shape, on the stored copy.
|
|
561
|
+
const accumulated = [...loaderWarnings, ...findRetryWithoutExplicitTimeout(definition)];
|
|
562
|
+
const msg = `Error loading extensions: ${err instanceof Error ? err.message : String(err)}`;
|
|
563
|
+
if (json) {
|
|
564
|
+
emitValidateJson({
|
|
565
|
+
valid: false,
|
|
566
|
+
mode: 'registered',
|
|
567
|
+
path: null,
|
|
568
|
+
workflowId: definition.id,
|
|
569
|
+
schemaVersion: stored.schema_version ?? null,
|
|
570
|
+
strictRequested: strict,
|
|
571
|
+
strictFailed: false,
|
|
572
|
+
diagnostics: accumulated,
|
|
573
|
+
errors: [msg],
|
|
574
|
+
checksNotRun: notRun,
|
|
575
|
+
extensionKeys: [],
|
|
576
|
+
});
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
for (const w of accumulated)
|
|
580
|
+
console.warn(renderLoaderWarning(w));
|
|
581
|
+
console.error(msg);
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
sentinelWarnings = loaded.sentinelWarnings;
|
|
585
|
+
try {
|
|
586
|
+
loadWorkflowFromStringWithDiagnostics(stripped, loaded.registry);
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
exitOnLoadFailure(err, json ? registeredCtx(definition.id, warningsOf(err)) : undefined);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// The file arm's tail, minus the adoption nudge (a stored copy is not where you edit;
|
|
594
|
+
// `--explain` is therefore inert in this mode, deliberately — no machinery for it).
|
|
595
|
+
const accumulated = [
|
|
596
|
+
...loaderWarnings,
|
|
597
|
+
...findRetryWithoutExplicitTimeout(definition),
|
|
598
|
+
...wrapSentinelWarnings(sentinelWarnings),
|
|
599
|
+
];
|
|
600
|
+
if (json) {
|
|
601
|
+
if (rejectOnErrorSeverity(accumulated)) {
|
|
602
|
+
emitValidateJson({
|
|
603
|
+
valid: false,
|
|
604
|
+
mode: 'registered',
|
|
605
|
+
path: null,
|
|
606
|
+
workflowId: definition.id,
|
|
607
|
+
schemaVersion: stored.schema_version ?? null,
|
|
608
|
+
strictRequested: strict,
|
|
609
|
+
strictFailed: false,
|
|
610
|
+
diagnostics: accumulated,
|
|
611
|
+
errors: [renderEscalationLine(accumulated)],
|
|
612
|
+
checksNotRun: notRun,
|
|
613
|
+
extensionKeys: [],
|
|
614
|
+
});
|
|
615
|
+
process.exit(1);
|
|
616
|
+
}
|
|
617
|
+
const strictFailed = strict && failsStrict(accumulated);
|
|
618
|
+
emitValidateJson({
|
|
619
|
+
valid: true,
|
|
620
|
+
mode: 'registered',
|
|
621
|
+
path: null,
|
|
622
|
+
workflowId: definition.id,
|
|
623
|
+
schemaVersion: stored.schema_version ?? null,
|
|
624
|
+
strictRequested: strict,
|
|
625
|
+
strictFailed,
|
|
626
|
+
diagnostics: accumulated,
|
|
627
|
+
errors: [],
|
|
628
|
+
checksNotRun: notRun,
|
|
629
|
+
extensionKeys: extensionKeysOf(definition),
|
|
630
|
+
});
|
|
631
|
+
if (strictFailed) {
|
|
632
|
+
process.exit(1);
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
348
635
|
}
|
|
349
|
-
// The extension-free arm's tail, minus the orphan-manifest check (there is no source tree to
|
|
350
|
-
// have one) and minus the adoption nudge (a stored copy is not where you edit; `--explain` is
|
|
351
|
-
// therefore inert in this mode, deliberately — no machinery for it).
|
|
352
|
-
const accumulated = [...loaderWarnings, ...findRetryWithoutExplicitTimeout(definition)];
|
|
353
636
|
if (rejectIfPolicyEscalates(accumulated)) {
|
|
354
637
|
process.exit(1);
|
|
355
638
|
}
|
|
356
|
-
const strictFailed = printValidationOutcome(definition, accumulated, strict);
|
|
639
|
+
const strictFailed = printValidationOutcome(definition, accumulated, strict, notRun.length, extensionKeysOf(definition));
|
|
357
640
|
if (strictFailed) {
|
|
358
641
|
process.exit(1);
|
|
359
642
|
}
|
|
@@ -364,15 +647,20 @@ export const validateCommand = new Command('validate')
|
|
|
364
647
|
.option('--extensions-module <path>', "Extensions module that REPLACES the workflow's declared 'extensions' modules (repair/override)")
|
|
365
648
|
.option('--strict', 'Exit non-zero if any loader warning is present (unknown keys, retry-without-timeout, sentinel credentials — issue #169)')
|
|
366
649
|
.option('--explain', 'Print the full per-step structured_output adoption detail instead of the one-line summary the default run prints (issue #422)')
|
|
650
|
+
.option('--json', 'Emit the result as JSON on stdout, and nothing else')
|
|
367
651
|
.description('Validate a workflow YAML file')
|
|
368
652
|
.action(async (inputPath, opts) => {
|
|
369
653
|
const strict = opts.strict === true;
|
|
370
654
|
const explain = opts.explain === true;
|
|
655
|
+
const json = opts.json === true;
|
|
371
656
|
// Exactly-one, checked FIRST and load-bearing: commander parses a `[path]` positional and
|
|
372
657
|
// a `--registered <id>` option happily together and enforces nothing between them
|
|
373
658
|
// (executed — both arrive). The flag is `--registered <id>` rather than an auto-detecting
|
|
374
659
|
// positional deliberately: nothing can reliably tell an id from a path, and guessing wrong
|
|
375
660
|
// means auditing something the operator did not name.
|
|
661
|
+
//
|
|
662
|
+
// issue #454: NOT under the contract — these two usage errors precede validation entirely
|
|
663
|
+
// (terraform-consistent) and stay human + exit 1 regardless of `--json`.
|
|
376
664
|
if (inputPath === undefined && opts.registered === undefined) {
|
|
377
665
|
console.error('Error: provide a workflow path, or --registered <id> to audit a stored definition.');
|
|
378
666
|
process.exit(1);
|
|
@@ -384,142 +672,128 @@ export const validateCommand = new Command('validate')
|
|
|
384
672
|
return;
|
|
385
673
|
}
|
|
386
674
|
if (opts.registered !== undefined) {
|
|
387
|
-
await validateRegistered(opts.registered, strict);
|
|
675
|
+
await validateRegistered(opts.registered, strict, json, opts.extensionsModule);
|
|
388
676
|
return;
|
|
389
677
|
}
|
|
390
678
|
const filePath = inputPath.endsWith('.yaml') || inputPath.endsWith('.yml')
|
|
391
679
|
? inputPath
|
|
392
680
|
: join(inputPath, 'workflow.yaml');
|
|
393
|
-
|
|
681
|
+
// ONE call (issue #553) — the path register and watch take, byte for byte: the file
|
|
682
|
+
// loader (read failure → the loader's `Failed to read workflow file:` sentence; profile
|
|
683
|
+
// resolution; the four context rules), the unconditional extensions pass with real-then-
|
|
684
|
+
// sentinel secret resolution, the config_schema pass 2. Two failure populations leave it,
|
|
685
|
+
// and each keeps its own sentence (issue #445): an ExtensionLoadError is extension or
|
|
686
|
+
// deployment territory (an unresolvable module, a malformed `realm.yaml`, the #123
|
|
687
|
+
// orphaned-manifest refusal) and says `Error loading extensions:`; everything else is the
|
|
688
|
+
// workflow's own invalidity, rendered by exitOnLoadFailure (a non-WorkflowError rethrows
|
|
689
|
+
// loud — the #123 doctrine).
|
|
690
|
+
let definition;
|
|
691
|
+
let warnings;
|
|
692
|
+
let manifest;
|
|
394
693
|
try {
|
|
395
|
-
|
|
694
|
+
({ definition, warnings, manifest } = await loadWorkflowForAdmission(filePath, {
|
|
695
|
+
...(opts.extensionsModule !== undefined ? { overrideModule: opts.extensionsModule } : {}),
|
|
696
|
+
surface: 'validate',
|
|
697
|
+
}));
|
|
396
698
|
}
|
|
397
699
|
catch (err) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
({
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// so nothing the author would otherwise see only on the NEXT run is withheld. Plain
|
|
432
|
-
// render: the escalation gate has not run, so printLoaderWarnings' `— REFUSED below`
|
|
433
|
-
// would name the wrong cause (test.ts's render comment, #450's reasoning). Unconditional:
|
|
434
|
-
// on the #123 non-WorkflowError rethrow population the warnings print before the loud
|
|
435
|
-
// crash — true statements either way. exitOnLoadFailure cannot print them twice: the
|
|
436
|
-
// orphan WorkflowError is minted bare in the CLI (load-project-extensions.ts), and the
|
|
437
|
-
// core `warnings` slot is set only by attachLoaderWarnings inside the core loader, which
|
|
438
|
-
// this throw never transits — which is exactly why this arm swallowed them until now.
|
|
439
|
-
for (const w of [...loaderWarnings, ...findRetryWithoutExplicitTimeout(definition)]) {
|
|
440
|
-
console.warn(renderLoaderWarning(w));
|
|
700
|
+
if (err instanceof ExtensionLoadError) {
|
|
701
|
+
// issue #463 — the workflow's own warnings first: pass-1's plus the retry advisory,
|
|
702
|
+
// the same set the success path counts minus the sentinel wraps, which come from the
|
|
703
|
+
// load that just failed — there is nothing to wrap. Plain render, not
|
|
704
|
+
// printLoaderWarnings — the escalation gate never ran on this arm (#540/#542).
|
|
705
|
+
//
|
|
706
|
+
// issue #454 — the errors[] convention's exception: this sentence ships WHOLE, with
|
|
707
|
+
// its `Error loading extensions: ` head — the #445 classification IS the composed
|
|
708
|
+
// message, not a channel prefix a caller prepends at print. Under `--json` the
|
|
709
|
+
// helper's two sentinel ⚠ lines may already have reached stderr (`console.warn`;
|
|
710
|
+
// register has no `--json`): stdout purity holds — #454's contract is stdout — and
|
|
711
|
+
// stderr may carry advisories on a contract arm (J7b pins it).
|
|
712
|
+
const failed = err.definition;
|
|
713
|
+
const accumulated = [
|
|
714
|
+
...(err.warnings ?? []),
|
|
715
|
+
...(failed !== undefined ? findRetryWithoutExplicitTimeout(failed) : []),
|
|
716
|
+
];
|
|
717
|
+
const msg = `Error loading extensions: ${err.message}`;
|
|
718
|
+
if (json) {
|
|
719
|
+
emitValidateJson({
|
|
720
|
+
valid: false,
|
|
721
|
+
mode: 'file',
|
|
722
|
+
path: inputPath,
|
|
723
|
+
workflowId: failed?.id ?? null,
|
|
724
|
+
schemaVersion: null,
|
|
725
|
+
strictRequested: strict,
|
|
726
|
+
strictFailed: false,
|
|
727
|
+
diagnostics: accumulated,
|
|
728
|
+
errors: [msg],
|
|
729
|
+
checksNotRun: [],
|
|
730
|
+
extensionKeys: [],
|
|
731
|
+
});
|
|
732
|
+
process.exit(1);
|
|
441
733
|
}
|
|
442
|
-
|
|
734
|
+
for (const w of accumulated)
|
|
735
|
+
console.warn(renderLoaderWarning(w));
|
|
736
|
+
console.error(msg);
|
|
737
|
+
process.exit(1);
|
|
443
738
|
}
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
739
|
+
// A pass-2 (config_schema) refusal carries the pass-1 definition beside it, so
|
|
740
|
+
// `workflow_id` is named exactly as before the collapse (round-2 Q9); a pass-1 refusal
|
|
741
|
+
// has no definition to name.
|
|
742
|
+
exitOnLoadFailure(err, json
|
|
743
|
+
? {
|
|
744
|
+
mode: 'file',
|
|
745
|
+
path: inputPath,
|
|
746
|
+
workflowId: admittedDefinitionOf(err)?.id ?? null,
|
|
747
|
+
schemaVersion: null,
|
|
748
|
+
diagnostics: warningsOf(err),
|
|
749
|
+
strictRequested: strict,
|
|
750
|
+
checksNotRun: [],
|
|
751
|
+
}
|
|
752
|
+
: undefined);
|
|
753
|
+
}
|
|
754
|
+
const accumulated = [...warnings, ...findRetryWithoutExplicitTimeout(definition)];
|
|
755
|
+
if (json) {
|
|
756
|
+
if (rejectOnErrorSeverity(accumulated)) {
|
|
757
|
+
emitValidateJson({
|
|
758
|
+
valid: false,
|
|
759
|
+
mode: 'file',
|
|
760
|
+
path: inputPath,
|
|
761
|
+
workflowId: definition.id,
|
|
762
|
+
schemaVersion: null,
|
|
763
|
+
strictRequested: strict,
|
|
764
|
+
strictFailed: false,
|
|
765
|
+
diagnostics: accumulated,
|
|
766
|
+
errors: [renderEscalationLine(accumulated)],
|
|
767
|
+
checksNotRun: [],
|
|
768
|
+
extensionKeys: [],
|
|
769
|
+
});
|
|
449
770
|
process.exit(1);
|
|
450
771
|
}
|
|
451
|
-
const strictFailed =
|
|
452
|
-
|
|
453
|
-
|
|
772
|
+
const strictFailed = strict && failsStrict(accumulated);
|
|
773
|
+
emitValidateJson({
|
|
774
|
+
valid: true,
|
|
775
|
+
mode: 'file',
|
|
776
|
+
path: inputPath,
|
|
777
|
+
workflowId: definition.id,
|
|
778
|
+
schemaVersion: null,
|
|
779
|
+
strictRequested: strict,
|
|
780
|
+
strictFailed,
|
|
781
|
+
diagnostics: accumulated,
|
|
782
|
+
errors: [],
|
|
783
|
+
checksNotRun: [],
|
|
784
|
+
extensionKeys: extensionKeysOf(definition),
|
|
785
|
+
});
|
|
786
|
+
// issue #422/#236: the Extensions manifest line and the nudge are both suppressed under
|
|
787
|
+
// --json — human-informational, not represented (additive later if ever wanted).
|
|
454
788
|
if (strictFailed) {
|
|
455
789
|
process.exit(1);
|
|
456
790
|
}
|
|
457
791
|
return;
|
|
458
792
|
}
|
|
459
|
-
// Extensions declared (or an override supplied): file-based two-pass validation.
|
|
460
|
-
//
|
|
461
|
-
// THREE tries, not one (issue #445). The old single catch spanned five concerns and
|
|
462
|
-
// rendered every one of them `Invalid: …` — including a user's unresolvable extension
|
|
463
|
-
// module, and including internal bugs. Each try now owns one failure population, and the
|
|
464
|
-
// reporting tail sits outside all of them.
|
|
465
|
-
let definition;
|
|
466
|
-
let pass1Warnings;
|
|
467
|
-
try {
|
|
468
|
-
// Pass 1: structural validation + extension resolution metadata (source_dir/trust_root).
|
|
469
|
-
// The universal, registry-independent structural load — its warnings are what we count.
|
|
470
|
-
({ definition, warnings: pass1Warnings } = loadWorkflowFromFileWithDiagnostics(filePath));
|
|
471
|
-
}
|
|
472
|
-
catch (err) {
|
|
473
|
-
exitOnLoadFailure(err);
|
|
474
|
-
}
|
|
475
|
-
let registry;
|
|
476
|
-
let manifest;
|
|
477
|
-
let sentinelWarnings;
|
|
478
|
-
try {
|
|
479
|
-
({ registry, manifest, sentinelWarnings } = await loadProjectExtensions(definition, {
|
|
480
|
-
...(opts.extensionsModule !== undefined ? { overrideModule: opts.extensionsModule } : {}),
|
|
481
|
-
secretMode: 'sentinel',
|
|
482
|
-
}));
|
|
483
|
-
}
|
|
484
|
-
catch (err) {
|
|
485
|
-
// issue #445 — a DIFFERENT population, and it gets its own sentence. Everything
|
|
486
|
-
// loadProjectExtensions throws is extension or deployment territory: an unresolvable
|
|
487
|
-
// module path, a failed import, a module whose default export is the wrong shape, a
|
|
488
|
-
// malformed `realm.yaml`, and the #123 orphaned-manifest refusal. None of those makes
|
|
489
|
-
// the WORKFLOW invalid, and calling them `Invalid:` sent an author to the wrong file.
|
|
490
|
-
// The sentence is `realm run`'s, verbatim (run.ts) — the sibling surface renders this
|
|
491
|
-
// identical failure class exactly so.
|
|
492
|
-
//
|
|
493
|
-
// issue #463 — the workflow's own warnings first: pass-1's plus the retry advisory, the
|
|
494
|
-
// same set the success path counts minus the sentinel wraps, which come from the load that
|
|
495
|
-
// just failed — there is nothing to wrap. Plain render (test.ts's render comment, #450's
|
|
496
|
-
// reasoning): the escalation gate has not run, so printLoaderWarnings' `— REFUSED below`
|
|
497
|
-
// would name the wrong cause.
|
|
498
|
-
for (const w of [...pass1Warnings, ...findRetryWithoutExplicitTimeout(definition)]) {
|
|
499
|
-
console.warn(renderLoaderWarning(w));
|
|
500
|
-
}
|
|
501
|
-
console.error(`Error loading extensions: ${err instanceof Error ? err.message : String(err)}`);
|
|
502
|
-
process.exit(1);
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
// Pass 2: step config validated against each resolved adapter's config_schema. Same
|
|
506
|
-
// content as pass 1, registry only adds config_schema checks — its warnings are proven
|
|
507
|
-
// identical to pass 1's, so they are deliberately discarded here (not collected) to avoid
|
|
508
|
-
// double-counting the same unknown key twice.
|
|
509
|
-
loadWorkflowFromFileWithDiagnostics(filePath, registry);
|
|
510
|
-
}
|
|
511
|
-
catch (err) {
|
|
512
|
-
exitOnLoadFailure(err);
|
|
513
|
-
}
|
|
514
|
-
const accumulated = [
|
|
515
|
-
...pass1Warnings,
|
|
516
|
-
...findRetryWithoutExplicitTimeout(definition),
|
|
517
|
-
...wrapSentinelWarnings(sentinelWarnings),
|
|
518
|
-
];
|
|
519
793
|
if (rejectIfPolicyEscalates(accumulated)) {
|
|
520
794
|
process.exit(1);
|
|
521
795
|
}
|
|
522
|
-
const strictFailed = printValidationOutcome(definition, accumulated, strict);
|
|
796
|
+
const strictFailed = printValidationOutcome(definition, accumulated, strict, 0, extensionKeysOf(definition));
|
|
523
797
|
if (manifest.modules.length > 0) {
|
|
524
798
|
console.log(`Extensions: ${manifest.modules.map((m) => m.declared).join(', ')} ` +
|
|
525
799
|
`(adapters: ${manifest.adapters.length}, handlers: ${manifest.handlers.length}, ` +
|