@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 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