@formigio/fazemos-cli 0.10.11 → 0.10.13

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';
@@ -2466,14 +2469,58 @@ commitments
2466
2469
  process.exit(1);
2467
2470
  }
2468
2471
  });
2472
+ // [F24 §3.6 / R16-R20] cm reopen — Restore an open state on a closed
2473
+ // commitment. The close event stays in the audit trail (a new audit_log
2474
+ // row records the reopen with from_state and to_state='open'). Agents
2475
+ // cannot reopen — the API's permission predicate naturally excludes
2476
+ // service principals (requireMemberId throws when req.member.id is null).
2477
+ commitments
2478
+ .command('reopen <id>')
2479
+ .description('Restore an open state on a closed commitment. The close event stays in the audit trail.')
2480
+ .option('--reason <text>', 'Optional human-readable note for the audit log (max 500 chars)')
2481
+ .option('--json', 'Print the raw API response as JSON (machine-readable)')
2482
+ .action(async (id, opts) => {
2483
+ try {
2484
+ const body = {};
2485
+ if (opts.reason)
2486
+ body.reason = opts.reason;
2487
+ const data = await api('PATCH', `/api/commitments/${id}/reopen`, body);
2488
+ if (opts.json) {
2489
+ console.log(JSON.stringify(data, null, 2));
2490
+ return;
2491
+ }
2492
+ if (data.noop) {
2493
+ console.log(chalk.yellow('This commitment is already open. Nothing to reopen.'));
2494
+ return;
2495
+ }
2496
+ const c = data.commitment;
2497
+ const prev = data.previousCompletedAt;
2498
+ const prevFmt = prev ? new Date(prev).toISOString().slice(0, 16).replace('T', ' ') : null;
2499
+ const tail = prevFmt ? ` The close on ${prevFmt} stays in the audit trail.` : '';
2500
+ console.log(chalk.green(`Reopened. Commitment ${c.id} is open again.${tail}`));
2501
+ }
2502
+ catch (err) {
2503
+ console.error(chalk.red(err.message));
2504
+ process.exit(1);
2505
+ }
2506
+ });
2507
+ // [F24 §3.4 / R13-R14] cm execute — gained `--repos` option for
2508
+ // commitment-source clone overrides (mirrors the top-level execute flag).
2509
+ // The server-side endpoint now reads agent_config.repos as a fallback
2510
+ // when no body.repos is supplied; this CLI flag is the operator-side
2511
+ // override for `fazemos cm execute -w <ws> -c <cm> --repos <repos>`.
2469
2512
  commitments
2470
2513
  .command('execute')
2471
2514
  .description('Trigger an agent execution for a commitment. The API determines which agent to use based on the commitment\'s configuration. For more control, use the top-level "execute" command instead.')
2472
2515
  .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
2473
2516
  .requiredOption('-c, --commitment <id>', 'Commitment ID (from "cm list" or "ws show" output)')
2517
+ .option('--repos <repos>', 'Comma-separated repo names to clone for this execution (overrides the agent\'s default agent_config.repos)', (v) => v.split(','))
2474
2518
  .action(async (opts) => {
2475
2519
  try {
2476
- const data = await api('POST', `/api/worksheets/${opts.worksheet}/commitments/${opts.commitment}/execute`);
2520
+ const body = {};
2521
+ if (opts.repos)
2522
+ body.repos = opts.repos;
2523
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/commitments/${opts.commitment}/execute`, body);
2477
2524
  console.log(chalk.green('Execution triggered'));
2478
2525
  if (data.execution?.id)
2479
2526
  console.log(` Execution ID: ${data.execution.id}`);
@@ -4718,7 +4765,8 @@ program
4718
4765
  .argument('<source-id>', 'Action, commitment, or pipeline step ID')
4719
4766
  .requiredOption('--agent <name>', 'Agent name or member ID')
4720
4767
  .option('--source-type <type>', 'Source type (action, commitment, pipeline_step)', 'action')
4721
- .option('--prompt <prompt>', 'Additional prompt context')
4768
+ .option('--prompt <prompt>', 'Operator instructions for this execution. Appended to the agent\'s built prompt as a labeled "## Operator Instructions" section. Takes precedence over runtime auto-completion instructions per the precedence rule (see architecture doc).')
4769
+ .option('--no-auto-complete', 'Suppress the runtime auto-completion instruction. The agent will not be told to mark the source done. Use this for "draft, don\'t close" workflows where a human reviews and locks. Default: off. Pipeline-step source: ignored with a warning.')
4722
4770
  .option('--repos <repos>', 'Comma-separated repo names to clone (overrides agent config)', (v) => v.split(','))
4723
4771
  .option('--model <model>', 'Model override (e.g., opus, sonnet)')
4724
4772
  .option('--budget <usd>', 'Max budget override in USD', parseNumber)
@@ -4729,6 +4777,22 @@ program
4729
4777
  console.error(chalk.red(`Invalid source type. Must be one of: ${validTypes.join(', ')}`));
4730
4778
  process.exit(1);
4731
4779
  }
4780
+ // [F24 §3.3 / EC3 / rul_no_auto_complete_pipeline_step_ignored]
4781
+ // --no-auto-complete is ignored for pipeline_step sources to preserve
4782
+ // back-compat for shipped pipeline executions. Layer-1 defense:
4783
+ // CLI prints a warning and does NOT forward the flag onto
4784
+ // context.noAutoComplete. Layer-2 defense lives in the agent's
4785
+ // buildSelfReportingInstructions, which gates the noAutoComplete
4786
+ // check on source_type !== 'pipeline_step' regardless.
4787
+ //
4788
+ // commander.js: `--no-auto-complete` produces opts.autoComplete=false
4789
+ // (NOT opts.noAutoComplete=true) because of the `--no-` prefix
4790
+ // convention. We surface that as the boolean `forwardSuppress` below.
4791
+ const operatorOptedOut = opts.autoComplete === false;
4792
+ if (operatorOptedOut && opts.sourceType === 'pipeline_step') {
4793
+ console.error(chalk.yellow('Warning: --no-auto-complete is ignored for pipeline_step sources; pipeline steps always auto-complete.'));
4794
+ }
4795
+ const forwardSuppress = operatorOptedOut && opts.sourceType !== 'pipeline_step';
4732
4796
  // Resolve agent name to member ID if not already a UUID
4733
4797
  let agentMemberId = opts.agent;
4734
4798
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(agentMemberId)) {
@@ -4765,8 +4829,24 @@ program
4765
4829
  url: `https://github.com/formigio/${r}.git`,
4766
4830
  branch: 'main',
4767
4831
  })),
4768
- cwd: '/workspace',
4769
4832
  };
4833
+ // [F24 §3.2 / D8 / rul_workspace_clone_contract] cwd defaulting per
4834
+ // resolved repo count. Pre-F24 the CLI hardcoded cwd='/workspace',
4835
+ // which is ALWAYS empty inside the runner (clones land at
4836
+ // /home/agent/development/<repo>). BUG20's symptom ("'.git' missing")
4837
+ // was just the cwd misalignment — the clone itself was already
4838
+ // correct. Fix:
4839
+ // single repo → /home/agent/development/<repo> (align with clone)
4840
+ // multi repo → unset, let runner pick first repo via its existing
4841
+ // fallback at fazemos-agent-runner.ts:1075
4842
+ // zero repos → /workspace (preserve chat_session and no-repo cases)
4843
+ if (repoNames.length === 1) {
4844
+ context.cwd = `/home/agent/development/${repoNames[0]}`;
4845
+ }
4846
+ else if (repoNames.length === 0) {
4847
+ context.cwd = '/workspace';
4848
+ }
4849
+ // multi-repo: leave context.cwd unset — runner fallback chooses
4770
4850
  // Resolve worksheet context for actions/commitments
4771
4851
  if (opts.sourceType === 'action' || opts.sourceType === 'commitment') {
4772
4852
  try {
@@ -4783,6 +4863,17 @@ program
4783
4863
  context.worksheetName = detail.worksheet?.name;
4784
4864
  context.worksheetPurpose = detail.worksheet?.purpose;
4785
4865
  context.actionDescription = match.description || match.name;
4866
+ // [F24 §3.3 / D3 / rul_action_completion_semantics]
4867
+ // Propagate the action's target_value into context so the
4868
+ // agent's buildSelfReportingInstructions can branch:
4869
+ // target_value null|1 → binary auto-complete with -v 1
4870
+ // target_value > 1 → emit a "non-binary target" note,
4871
+ // NO auto-completion command
4872
+ // Only meaningful for source_type='action'; we set it only
4873
+ // here (the action-resolution branch).
4874
+ if (opts.sourceType === 'action') {
4875
+ context.actionTargetValue = match.target_value ?? null;
4876
+ }
4786
4877
  // Include linked outcomes
4787
4878
  if (detail.outcomes?.length) {
4788
4879
  context.outcomes = detail.outcomes.map((o) => ({
@@ -4805,6 +4896,12 @@ program
4805
4896
  }
4806
4897
  if (opts.prompt)
4807
4898
  context.prompt = opts.prompt;
4899
+ // [F24 §3.3 / D2] Forward --no-auto-complete intent. The CLI guard
4900
+ // above suppresses this for pipeline_step (forwardSuppress is false
4901
+ // in that case). Agent's buildSelfReportingInstructions reads
4902
+ // context.noAutoComplete; absent/false → default behavior.
4903
+ if (forwardSuppress)
4904
+ context.noAutoComplete = true;
4808
4905
  const body = {
4809
4906
  sourceType: opts.sourceType,
4810
4907
  sourceId,
@@ -7453,6 +7550,135 @@ docs
7453
7550
  handleScopedError(err);
7454
7551
  }
7455
7552
  });
7553
+ // ── Spec-artifact validation (TOOL-1) ──────────────────────────────────
7554
+ //
7555
+ // Two top-level groups:
7556
+ // fazemos yaml validate <path> Generic YAML parse check
7557
+ // fazemos manifest validate <path> Architecture Design Manifest check
7558
+ //
7559
+ // Both support --json for structured output. Exit code 1 on errors, 0 on
7560
+ // warnings/info only. See CLAUDE.md "TOOL-1" for scope.
7561
+ const yamlGroup = program.command('yaml').description('Generic YAML file utilities. Offline; no API calls. Use as a cheap ' +
7562
+ 'author-time or CI gate before committing a YAML file or feeding it into ' +
7563
+ 'another tool. For Architecture Design Manifests, prefer `fazemos manifest ' +
7564
+ 'validate` instead — it runs YAML parse plus structural and custom checks.');
7565
+ yamlGroup
7566
+ .command('validate')
7567
+ .description('Parse a YAML file and report parse errors with line/column. Cheap pre-commit / CI gate.')
7568
+ .argument('<path>', 'Path to a YAML file (.yaml or .yml). Absolute or relative to CWD.')
7569
+ .option('--json', 'Emit structured JSON output instead of human-readable text. See --help for shape.')
7570
+ .addHelpText('after', `
7571
+ Rule slugs (stable; safe to grep in --json output):
7572
+ read.not_found File at <path> does not exist
7573
+ read.not_a_file <path> exists but is not a regular file
7574
+ read.io_error Read failed (permissions, etc.)
7575
+ yaml.parse_error js-yaml could not parse the document
7576
+
7577
+ Exit codes:
7578
+ 0 YAML parsed cleanly (no error findings)
7579
+ 1 Parse or read error finding present
7580
+
7581
+ JSON output shape (--json):
7582
+ {
7583
+ "source": "<path>",
7584
+ "ok": true | false,
7585
+ "summary": { "errors": <int>, "warnings": <int>, "infos": <int> },
7586
+ "findings": [
7587
+ { "severity": "error"|"warning"|"info",
7588
+ "rule": "<rule-slug>",
7589
+ "message": "<human-readable>",
7590
+ "path": "<dotted-path-into-doc>", // optional
7591
+ "line": <int>, // optional (parse errors)
7592
+ "column": <int> } // optional (parse errors)
7593
+ ]
7594
+ }
7595
+
7596
+ Examples:
7597
+ fazemos yaml validate ./config.yaml
7598
+ fazemos yaml validate ./fixture.yml --json | jq '.findings[]'`)
7599
+ .action((path, opts) => {
7600
+ const result = loadYaml(path);
7601
+ const summary = summarize(result.findings);
7602
+ if (opts.json)
7603
+ printJson(result.source, result.findings, summary);
7604
+ else
7605
+ printFindings(result.source, result.findings, summary);
7606
+ if (summary.errors > 0)
7607
+ process.exit(1);
7608
+ });
7609
+ const manifestGroup = program.command('manifest').description('Architecture Design Manifest validation (the YAML companion to a tech ' +
7610
+ 'spec, conventionally at specs/tech/<area>/<feature>-manifest.yaml). ' +
7611
+ 'Offline; no API calls. Designed to run at three points: (1) author time ' +
7612
+ 'before commit, (2) in agent prompts (Marco / Dex) before review tokens ' +
7613
+ 'are spent, (3) in CI on PRs touching a manifest. Catches the recurring ' +
7614
+ 'miss class — audit-summary math, AC traceability, duplicate IDs — ' +
7615
+ 'without a workspace-level toolchain.');
7616
+ manifestGroup
7617
+ .command('validate')
7618
+ .description('Validate an Architecture Design Manifest: YAML parse + structural schema + custom checks (audit math, AC traceability, duplicate IDs).')
7619
+ .argument('<path>', 'Path to a manifest YAML file. Convention: specs/tech/<area>/<feature>-manifest.yaml inside a workspace clone. Absolute paths also work.')
7620
+ .option('--json', 'Emit structured JSON output instead of human-readable text. See --help for shape.')
7621
+ .addHelpText('after', `
7622
+ What runs (in order):
7623
+ 1. js-yaml parse (rule slug: yaml.parse_error)
7624
+ 2. permissive structural schema (ajv) (rule slugs: schema.*)
7625
+ 3. custom checks (rule slugs: audit.* / trace.* / ids.* / manifest.*)
7626
+
7627
+ The schema requires feature.id + feature.title. It accepts both manifest shape
7628
+ variants seen in the wild — entry_points as map ({added, modified, replaced,
7629
+ unchanged, audit_summary}) OR as a flat list, traceability values as string OR
7630
+ object OR array-of-rows, rules as map OR array-of-{id, ...} entries.
7631
+
7632
+ Rule slugs (stable; safe to grep in --json output):
7633
+ audit.sum_mismatch audit_summary.{added+modified+replaced+unchanged} ≠ total_in_this_manifest
7634
+ audit.count_vs_array audit_summary declared count ≠ actual entry_points.<bucket>[] length
7635
+ trace.ac_task_missing traceability.AC*.task references a task ID with no match in implementation.*.tasks
7636
+ (recurses through implementation.sequencing.* and similar nested buckets)
7637
+ ids.duplicate_task same task id in multiple implementation buckets
7638
+ ids.duplicate duplicate id in edge_cases / risks / phase_1_gate.questions
7639
+ helper.callers_duplicate (warning) api.internal_helper.callers lists same caller twice
7640
+ schema.required required field missing (feature.id, feature.title)
7641
+ schema.type / schema.anyOf value has wrong shape
7642
+ manifest.version_missing (warning) add manifest_version: "0.1" at the top of the file
7643
+ manifest.version_unknown (warning) manifest_version is newer than this CLI knows; checks still ran
7644
+ manifest.shape root is not a YAML mapping
7645
+ read.not_found / read.io_error / yaml.parse_error file-level failures
7646
+
7647
+ Exit codes:
7648
+ 0 no error-severity findings (warnings + infos OK)
7649
+ 1 any error-severity finding present
7650
+
7651
+ JSON output shape (--json) — same as \`yaml validate --json\`:
7652
+ {
7653
+ "source": "<path>",
7654
+ "ok": true | false,
7655
+ "summary": { "errors": <int>, "warnings": <int>, "infos": <int> },
7656
+ "findings": [ { "severity", "rule", "message", "path"?, "line"?, "column"? } ]
7657
+ }
7658
+
7659
+ When to invoke:
7660
+ - Author time, before commit (catches the recurring miss class earliest)
7661
+ - Inside Marco / Dex agent prompts before review tokens are spent
7662
+ - In CI on any PR that touches a *-manifest.yaml file
7663
+
7664
+ Examples:
7665
+ fazemos manifest validate ./specs/tech/platform/I45-...-manifest.yaml
7666
+ fazemos manifest validate ./manifest.yaml --json | jq '.findings | map(select(.severity=="error"))'
7667
+ fazemos manifest validate ./manifest.yaml --json | jq -r '.findings[] | "\\(.severity) \\(.rule) \\(.message)"'`)
7668
+ .action((path, opts) => {
7669
+ const loaded = loadYaml(path);
7670
+ const findings = [...loaded.findings];
7671
+ if (loaded.ok) {
7672
+ findings.push(...validateManifest(loaded.value));
7673
+ }
7674
+ const summary = summarize(findings);
7675
+ if (opts.json)
7676
+ printJson(loaded.source, findings, summary);
7677
+ else
7678
+ printFindings(loaded.source, findings, summary);
7679
+ if (summary.errors > 0)
7680
+ process.exit(1);
7681
+ });
7456
7682
  // Skip auto-parse only when running under Vitest (which sets process.env.VITEST).
7457
7683
  // Tests import `program` and drive it via `program.parseAsync(...)` after mocking
7458
7684
  // `./api.js`. In every other context — direct invocation, npx tsx, OR the bin