@formigio/fazemos-cli 0.10.10 → 0.10.12
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/index.js +164 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest/checks.d.ts +6 -0
- package/dist/manifest/checks.js +356 -0
- package/dist/manifest/checks.js.map +1 -0
- package/dist/manifest/schema.d.ts +314 -0
- package/dist/manifest/schema.js +208 -0
- package/dist/manifest/schema.js.map +1 -0
- package/dist/yaml/format.d.ts +5 -0
- package/dist/yaml/format.js +41 -0
- package/dist/yaml/format.js.map +1 -0
- package/dist/yaml/load.d.ts +28 -0
- package/dist/yaml/load.js +74 -0
- package/dist/yaml/load.js.map +1 -0
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ getActiveProjectId, setActiveProjectId, clearActiveProjectId, findProjectBySlug,
|
|
|
7
7
|
import { login, signup, confirmSignup, adminLogin } from './auth.js';
|
|
8
8
|
import { api, ApiError, refreshAuthMeCache, invalidateAuthMeCache } from './api.js';
|
|
9
9
|
import { isProjectConnectionUnavailable, renderProjectConnectionUnavailableCopy, } from './connectionErrorCopy.js';
|
|
10
|
+
import { loadYaml, summarize } from './yaml/load.js';
|
|
11
|
+
import { printFindings, printJson } from './yaml/format.js';
|
|
12
|
+
import { validateManifest } from './manifest/checks.js';
|
|
10
13
|
import { execSync } from 'child_process';
|
|
11
14
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
12
15
|
import { fileURLToPath } from 'url';
|
|
@@ -26,6 +29,13 @@ function parseStreamSilenceAbortMs(val) {
|
|
|
26
29
|
}
|
|
27
30
|
return n;
|
|
28
31
|
}
|
|
32
|
+
function parseEvalMaxTurns(val) {
|
|
33
|
+
const n = Number(val);
|
|
34
|
+
if (!Number.isInteger(n) || n < 8 || n > 30) {
|
|
35
|
+
throw new Error(`eval_max_turns must be an integer between 8 and 30 (got ${val})`);
|
|
36
|
+
}
|
|
37
|
+
return n;
|
|
38
|
+
}
|
|
29
39
|
function formatMsHuman(ms) {
|
|
30
40
|
if (ms >= 60000) {
|
|
31
41
|
const min = ms / 60000;
|
|
@@ -2823,6 +2833,9 @@ templates
|
|
|
2823
2833
|
else if (step.outputs?.length) {
|
|
2824
2834
|
console.log(' Inputs: none');
|
|
2825
2835
|
}
|
|
2836
|
+
if (step.eval_max_turns != null) {
|
|
2837
|
+
console.log(` Eval Max Turns: ${step.eval_max_turns}`);
|
|
2838
|
+
}
|
|
2826
2839
|
if (step.stream_silence_abort_ms) {
|
|
2827
2840
|
console.log(` Stream Silence Abort: ${step.stream_silence_abort_ms.toLocaleString()}ms (${formatMsHuman(step.stream_silence_abort_ms)})`);
|
|
2828
2841
|
}
|
|
@@ -3179,6 +3192,7 @@ templates
|
|
|
3179
3192
|
.option('--sort-order <n>', 'Set sort_order directly (lower-level escape hatch)', parseNumber)
|
|
3180
3193
|
.option('--agent-config <json>', 'Per-step agent config overrides as JSON (e.g., \'{"model":"opus","maxBudgetUsd":20}\'). Overrides agent member defaults for: model, maxBudgetUsd, maxTurns, timeoutMs, cwd, repos')
|
|
3181
3194
|
.option('--stream-silence-abort-ms <ms>', 'Stream silence abort threshold in milliseconds (30000-1800000)', parseStreamSilenceAbortMs)
|
|
3195
|
+
.option('--eval-max-turns <n>', 'Evaluator turn budget for tool-enabled (agent) evals on this step (8-30). Has no effect on script-typed steps (rejected by API).', parseEvalMaxTurns)
|
|
3182
3196
|
.action(async (templateId, opts) => {
|
|
3183
3197
|
try {
|
|
3184
3198
|
if (!VALID_STEP_TYPES.includes(opts.type)) {
|
|
@@ -3240,6 +3254,8 @@ templates
|
|
|
3240
3254
|
step.agent_config = parseJson(opts.agentConfig, '--agent-config');
|
|
3241
3255
|
if (opts.streamSilenceAbortMs !== undefined)
|
|
3242
3256
|
step.stream_silence_abort_ms = opts.streamSilenceAbortMs;
|
|
3257
|
+
if (opts.evalMaxTurns !== undefined)
|
|
3258
|
+
step.eval_max_turns = opts.evalMaxTurns;
|
|
3243
3259
|
// Positioning: --after or --sort-order
|
|
3244
3260
|
if (opts.after && opts.sortOrder !== undefined) {
|
|
3245
3261
|
console.error(chalk.red('Cannot use both --after and --sort-order'));
|
|
@@ -3268,6 +3284,9 @@ templates
|
|
|
3268
3284
|
if (step.stream_silence_abort_ms) {
|
|
3269
3285
|
console.log(` Stream Silence Abort: ${step.stream_silence_abort_ms.toLocaleString()}ms (${formatMsHuman(step.stream_silence_abort_ms)})`);
|
|
3270
3286
|
}
|
|
3287
|
+
if (step.eval_max_turns != null) {
|
|
3288
|
+
console.log(` Eval Max Turns: ${step.eval_max_turns}`);
|
|
3289
|
+
}
|
|
3271
3290
|
}
|
|
3272
3291
|
catch (err) {
|
|
3273
3292
|
console.error(chalk.red(err.message));
|
|
@@ -3337,12 +3356,20 @@ templates
|
|
|
3337
3356
|
.option('--sections <text>', 'Agent instructions / step content. Use @filepath to load from a file (e.g., --sections @steps/review.md)')
|
|
3338
3357
|
.option('--agent-config <json>', 'Per-step agent config overrides as JSON (merges with existing)')
|
|
3339
3358
|
.option('--stream-silence-abort-ms <ms>', 'Stream silence abort threshold in milliseconds (30000-1800000)', parseStreamSilenceAbortMs)
|
|
3359
|
+
.option('--eval-max-turns <n>', 'Evaluator turn budget for tool-enabled (agent) evals on this step (8-30). Mutually exclusive with --clear-eval-max-turns.', parseEvalMaxTurns)
|
|
3360
|
+
.option('--clear-eval-max-turns', 'Remove a previously-set eval_max_turns from this step, reverting to runner default. Mutually exclusive with --eval-max-turns.')
|
|
3340
3361
|
.action(async (templateId, opts) => {
|
|
3341
3362
|
try {
|
|
3363
|
+
if (opts.evalMaxTurns !== undefined && opts.clearEvalMaxTurns) {
|
|
3364
|
+
console.error(chalk.red('--eval-max-turns and --clear-eval-max-turns are mutually exclusive'));
|
|
3365
|
+
process.exit(1);
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3342
3368
|
const hasUpdate = opts.name || opts.type || opts.role || opts.description != null
|
|
3343
3369
|
|| opts.reviewer != null || opts.maxReviewCycles != null || opts.parallelGroup != null
|
|
3344
3370
|
|| opts.image || opts.command || opts.timeout !== undefined || opts.workingDir || opts.env
|
|
3345
|
-
|| opts.agentConfig || opts.sections != null || opts.streamSilenceAbortMs !== undefined
|
|
3371
|
+
|| opts.agentConfig || opts.sections != null || opts.streamSilenceAbortMs !== undefined
|
|
3372
|
+
|| opts.evalMaxTurns !== undefined || opts.clearEvalMaxTurns;
|
|
3346
3373
|
if (!hasUpdate) {
|
|
3347
3374
|
console.error(chalk.red('Provide at least one field to update'));
|
|
3348
3375
|
process.exit(1);
|
|
@@ -3415,6 +3442,13 @@ templates
|
|
|
3415
3442
|
if (opts.streamSilenceAbortMs !== undefined) {
|
|
3416
3443
|
step.stream_silence_abort_ms = opts.streamSilenceAbortMs;
|
|
3417
3444
|
}
|
|
3445
|
+
// eval_max_turns: set or clear (mutually exclusive — checked above)
|
|
3446
|
+
if (opts.evalMaxTurns !== undefined) {
|
|
3447
|
+
step.eval_max_turns = opts.evalMaxTurns;
|
|
3448
|
+
}
|
|
3449
|
+
else if (opts.clearEvalMaxTurns) {
|
|
3450
|
+
delete step.eval_max_turns;
|
|
3451
|
+
}
|
|
3418
3452
|
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
3419
3453
|
console.log(chalk.green(`Updated step: ${step.name}`));
|
|
3420
3454
|
}
|
|
@@ -7422,6 +7456,135 @@ docs
|
|
|
7422
7456
|
handleScopedError(err);
|
|
7423
7457
|
}
|
|
7424
7458
|
});
|
|
7459
|
+
// ── Spec-artifact validation (TOOL-1) ──────────────────────────────────
|
|
7460
|
+
//
|
|
7461
|
+
// Two top-level groups:
|
|
7462
|
+
// fazemos yaml validate <path> Generic YAML parse check
|
|
7463
|
+
// fazemos manifest validate <path> Architecture Design Manifest check
|
|
7464
|
+
//
|
|
7465
|
+
// Both support --json for structured output. Exit code 1 on errors, 0 on
|
|
7466
|
+
// warnings/info only. See CLAUDE.md "TOOL-1" for scope.
|
|
7467
|
+
const yamlGroup = program.command('yaml').description('Generic YAML file utilities. Offline; no API calls. Use as a cheap ' +
|
|
7468
|
+
'author-time or CI gate before committing a YAML file or feeding it into ' +
|
|
7469
|
+
'another tool. For Architecture Design Manifests, prefer `fazemos manifest ' +
|
|
7470
|
+
'validate` instead — it runs YAML parse plus structural and custom checks.');
|
|
7471
|
+
yamlGroup
|
|
7472
|
+
.command('validate')
|
|
7473
|
+
.description('Parse a YAML file and report parse errors with line/column. Cheap pre-commit / CI gate.')
|
|
7474
|
+
.argument('<path>', 'Path to a YAML file (.yaml or .yml). Absolute or relative to CWD.')
|
|
7475
|
+
.option('--json', 'Emit structured JSON output instead of human-readable text. See --help for shape.')
|
|
7476
|
+
.addHelpText('after', `
|
|
7477
|
+
Rule slugs (stable; safe to grep in --json output):
|
|
7478
|
+
read.not_found File at <path> does not exist
|
|
7479
|
+
read.not_a_file <path> exists but is not a regular file
|
|
7480
|
+
read.io_error Read failed (permissions, etc.)
|
|
7481
|
+
yaml.parse_error js-yaml could not parse the document
|
|
7482
|
+
|
|
7483
|
+
Exit codes:
|
|
7484
|
+
0 YAML parsed cleanly (no error findings)
|
|
7485
|
+
1 Parse or read error finding present
|
|
7486
|
+
|
|
7487
|
+
JSON output shape (--json):
|
|
7488
|
+
{
|
|
7489
|
+
"source": "<path>",
|
|
7490
|
+
"ok": true | false,
|
|
7491
|
+
"summary": { "errors": <int>, "warnings": <int>, "infos": <int> },
|
|
7492
|
+
"findings": [
|
|
7493
|
+
{ "severity": "error"|"warning"|"info",
|
|
7494
|
+
"rule": "<rule-slug>",
|
|
7495
|
+
"message": "<human-readable>",
|
|
7496
|
+
"path": "<dotted-path-into-doc>", // optional
|
|
7497
|
+
"line": <int>, // optional (parse errors)
|
|
7498
|
+
"column": <int> } // optional (parse errors)
|
|
7499
|
+
]
|
|
7500
|
+
}
|
|
7501
|
+
|
|
7502
|
+
Examples:
|
|
7503
|
+
fazemos yaml validate ./config.yaml
|
|
7504
|
+
fazemos yaml validate ./fixture.yml --json | jq '.findings[]'`)
|
|
7505
|
+
.action((path, opts) => {
|
|
7506
|
+
const result = loadYaml(path);
|
|
7507
|
+
const summary = summarize(result.findings);
|
|
7508
|
+
if (opts.json)
|
|
7509
|
+
printJson(result.source, result.findings, summary);
|
|
7510
|
+
else
|
|
7511
|
+
printFindings(result.source, result.findings, summary);
|
|
7512
|
+
if (summary.errors > 0)
|
|
7513
|
+
process.exit(1);
|
|
7514
|
+
});
|
|
7515
|
+
const manifestGroup = program.command('manifest').description('Architecture Design Manifest validation (the YAML companion to a tech ' +
|
|
7516
|
+
'spec, conventionally at specs/tech/<area>/<feature>-manifest.yaml). ' +
|
|
7517
|
+
'Offline; no API calls. Designed to run at three points: (1) author time ' +
|
|
7518
|
+
'before commit, (2) in agent prompts (Marco / Dex) before review tokens ' +
|
|
7519
|
+
'are spent, (3) in CI on PRs touching a manifest. Catches the recurring ' +
|
|
7520
|
+
'miss class — audit-summary math, AC traceability, duplicate IDs — ' +
|
|
7521
|
+
'without a workspace-level toolchain.');
|
|
7522
|
+
manifestGroup
|
|
7523
|
+
.command('validate')
|
|
7524
|
+
.description('Validate an Architecture Design Manifest: YAML parse + structural schema + custom checks (audit math, AC traceability, duplicate IDs).')
|
|
7525
|
+
.argument('<path>', 'Path to a manifest YAML file. Convention: specs/tech/<area>/<feature>-manifest.yaml inside a workspace clone. Absolute paths also work.')
|
|
7526
|
+
.option('--json', 'Emit structured JSON output instead of human-readable text. See --help for shape.')
|
|
7527
|
+
.addHelpText('after', `
|
|
7528
|
+
What runs (in order):
|
|
7529
|
+
1. js-yaml parse (rule slug: yaml.parse_error)
|
|
7530
|
+
2. permissive structural schema (ajv) (rule slugs: schema.*)
|
|
7531
|
+
3. custom checks (rule slugs: audit.* / trace.* / ids.* / manifest.*)
|
|
7532
|
+
|
|
7533
|
+
The schema requires feature.id + feature.title. It accepts both manifest shape
|
|
7534
|
+
variants seen in the wild — entry_points as map ({added, modified, replaced,
|
|
7535
|
+
unchanged, audit_summary}) OR as a flat list, traceability values as string OR
|
|
7536
|
+
object OR array-of-rows, rules as map OR array-of-{id, ...} entries.
|
|
7537
|
+
|
|
7538
|
+
Rule slugs (stable; safe to grep in --json output):
|
|
7539
|
+
audit.sum_mismatch audit_summary.{added+modified+replaced+unchanged} ≠ total_in_this_manifest
|
|
7540
|
+
audit.count_vs_array audit_summary declared count ≠ actual entry_points.<bucket>[] length
|
|
7541
|
+
trace.ac_task_missing traceability.AC*.task references a task ID with no match in implementation.*.tasks
|
|
7542
|
+
(recurses through implementation.sequencing.* and similar nested buckets)
|
|
7543
|
+
ids.duplicate_task same task id in multiple implementation buckets
|
|
7544
|
+
ids.duplicate duplicate id in edge_cases / risks / phase_1_gate.questions
|
|
7545
|
+
helper.callers_duplicate (warning) api.internal_helper.callers lists same caller twice
|
|
7546
|
+
schema.required required field missing (feature.id, feature.title)
|
|
7547
|
+
schema.type / schema.anyOf value has wrong shape
|
|
7548
|
+
manifest.version_missing (warning) add manifest_version: "0.1" at the top of the file
|
|
7549
|
+
manifest.version_unknown (warning) manifest_version is newer than this CLI knows; checks still ran
|
|
7550
|
+
manifest.shape root is not a YAML mapping
|
|
7551
|
+
read.not_found / read.io_error / yaml.parse_error file-level failures
|
|
7552
|
+
|
|
7553
|
+
Exit codes:
|
|
7554
|
+
0 no error-severity findings (warnings + infos OK)
|
|
7555
|
+
1 any error-severity finding present
|
|
7556
|
+
|
|
7557
|
+
JSON output shape (--json) — same as \`yaml validate --json\`:
|
|
7558
|
+
{
|
|
7559
|
+
"source": "<path>",
|
|
7560
|
+
"ok": true | false,
|
|
7561
|
+
"summary": { "errors": <int>, "warnings": <int>, "infos": <int> },
|
|
7562
|
+
"findings": [ { "severity", "rule", "message", "path"?, "line"?, "column"? } ]
|
|
7563
|
+
}
|
|
7564
|
+
|
|
7565
|
+
When to invoke:
|
|
7566
|
+
- Author time, before commit (catches the recurring miss class earliest)
|
|
7567
|
+
- Inside Marco / Dex agent prompts before review tokens are spent
|
|
7568
|
+
- In CI on any PR that touches a *-manifest.yaml file
|
|
7569
|
+
|
|
7570
|
+
Examples:
|
|
7571
|
+
fazemos manifest validate ./specs/tech/platform/I45-...-manifest.yaml
|
|
7572
|
+
fazemos manifest validate ./manifest.yaml --json | jq '.findings | map(select(.severity=="error"))'
|
|
7573
|
+
fazemos manifest validate ./manifest.yaml --json | jq -r '.findings[] | "\\(.severity) \\(.rule) \\(.message)"'`)
|
|
7574
|
+
.action((path, opts) => {
|
|
7575
|
+
const loaded = loadYaml(path);
|
|
7576
|
+
const findings = [...loaded.findings];
|
|
7577
|
+
if (loaded.ok) {
|
|
7578
|
+
findings.push(...validateManifest(loaded.value));
|
|
7579
|
+
}
|
|
7580
|
+
const summary = summarize(findings);
|
|
7581
|
+
if (opts.json)
|
|
7582
|
+
printJson(loaded.source, findings, summary);
|
|
7583
|
+
else
|
|
7584
|
+
printFindings(loaded.source, findings, summary);
|
|
7585
|
+
if (summary.errors > 0)
|
|
7586
|
+
process.exit(1);
|
|
7587
|
+
});
|
|
7425
7588
|
// Skip auto-parse only when running under Vitest (which sets process.env.VITEST).
|
|
7426
7589
|
// Tests import `program` and drive it via `program.parseAsync(...)` after mocking
|
|
7427
7590
|
// `./api.js`. In every other context — direct invocation, npx tsx, OR the bin
|