@gluecharm-lab/easyspecs-cli 0.0.16 → 0.0.17

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/README.md CHANGED
@@ -219,7 +219,7 @@ These appear **before** the subcommand (everything after the first non-flag toke
219
219
  | ---- | ------ |
220
220
  | `--cwd <dir>` | Repository root for git resolution and paths (default: current working directory). |
221
221
  | `--ci` | Non-interactive mode; affects merged settings (e.g. Factory outer-iteration default). **`EASYSPECS_CI` is not read** — use this flag. |
222
- | `--json` | On supported exits, one JSON summary line on stdout. |
222
+ | `--json` | On supported exits, one JSON summary line on stdout. On **non-zero** exits, the line includes **`exitCode`** (number) and **`exitMeaning`** (one-line classification) alongside any command-specific fields. **`analysis`** validation failures also include **`factoryFailures`** (per failed factory phase), **`failurePhase`**, and **`validationExitId`**. |
223
223
  | `--verbose` | Extra stderr logging where implemented. |
224
224
  | `--api-base-url <url>` | System Manager API origin for this process (overrides `easyspecs.apiBaseUrl`). |
225
225
  | `--session-path <file>` | Session JSON path for **this process only** (overrides **`easyspecs.cliSessionPath`**); does not rewrite **`config.json`** unless you use **`auth login`** tail **`--session-path`** as documented under **Auth**. |
package/commands.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # EasySpecs CLI — commands, flags, and configuration
2
2
 
3
+ **Error codes:** OS exit codes, structured **`failureExitId`** values, and JSON field semantics are catalogued in **[`error-code.md`](./error-code.md)** (maintained with CLI changes).
4
+
3
5
  Published package: **`@gluecharm-lab/easyspecs-cli`** (`easyspecs-cli`). Source of truth for routing and option registration: [`src/cli/cliProgram.ts`](../../src/cli/cliProgram.ts), command dispatch and business flow wiring: [`src/cli/main.ts`](../../src/cli/main.ts), merged settings: [`mergeEasyspecsCliSettings`](../../src/cli/cliSettings.ts), per-repo config: [`src/config/easyspecsConfigFile.ts`](../../src/config/easyspecsConfigFile.ts), CLI API URL resolution: [`src/easyspecsApiBaseUrlCli.ts`](../../src/easyspecsApiBaseUrlCli.ts). The VS Code extension resolves API URLs via [`src/apiBaseUrlResolve.ts`](../../src/apiBaseUrlResolve.ts); **`easyspecs-cli` does not use that chain** for System Manager origin (**SRS-43 R30 / R34**).
4
6
 
5
7
  ### Vocabulary (SRS-53)
@@ -20,6 +22,22 @@ Quick usage:
20
22
  easyspecs-cli help
21
23
  ```
22
24
 
25
+ ### Factory validation reporting (**SRS-57**)
26
+
27
+ When **`easyspecs-cli analysis`** exits with **validation** (**`5`**), human stderr prints a short banner and **one bullet per failed factory phase** (readable label, **`validationExitId`** **`5.1`–`5.9`**, normative **`title`**, optional **`detail`**). **`--json`** adds:
28
+
29
+ | JSON key | Meaning |
30
+ | -------- | ------- |
31
+ | **`factoryFailures`** | Array of **`{ factory, phase, exitCode, failureExitId, title, validationExitId? (when exit 5), detail?, validationSubcode? }`** — **`failureExitId`** is the canonical **`major.minor`** id (SRS-58); see [`error-code.md`](./error-code.md). |
32
+ | **`failurePhase`** | Last failed phase in macro order (alias for scripts). |
33
+ | **`validationExitId`** | Matching **`5.x`** for **`failurePhase`**. |
34
+ | **`exitMeaning`** | First failure **`title`**, or that title plus “and *N* other pipeline phases failed”. |
35
+ | **`error`** | Orchestrator message; if several phases failed, a short appendix lists extra **`validationExitId`**s (see [`factoryValidationFailures.ts`](../../src/factory/factoryValidationFailures.ts)). |
36
+
37
+ **`validationExitId` → `phase` map:** **`5.1`** `create_analysis_worktree`, **`5.2`** `materialize_opencode_agents`, **`5.3`** `synthesis_convergence`, **`5.4`** `reference_coverage`, **`5.5`** `zero_reference_remediation_convergence`, **`5.6`** `reference_coverage_execution_report`, **`5.7`** `link_mapping_pipeline`, **`5.8`** `assemble_application_context_index`, **`5.9`** `backend_context_sync`, **`5.0`** unknown / unattributed. Normative **`title`** strings and readable labels: [`.gluecharm/docs/srs/srs-57.md`](../../.gluecharm/docs/srs/srs-57.md).
38
+
39
+ **Optional `validationSubcode` (v1):** **`R5_MACRO_PING_PONG`** when synthesis **`detail`** notes **R5** unstable convergence; **`COVERAGE_PERCENT_THRESHOLD`** when reference-coverage **`detail`** matches the strict percent gate pattern.
40
+
23
41
  ---
24
42
 
25
43
  ## Global options
@@ -30,7 +48,7 @@ These flags may appear **before** the subcommand. Parsing and command registrati
30
48
  |------|--------|
31
49
  | `--cwd <dir>` | Repository root for git resolution and file paths (default: current working directory). |
32
50
  | `--ci` | Non-interactive mode; feeds into merged settings (e.g. Factory outer-iteration default when unlimited). **`EASYSPECS_CI` is not read** — use this flag in CI. |
33
- | `--json` | On supported exits, prints one JSON summary line on stdout; suppresses some human-oriented stderr unless `--verbose`. |
51
+ | `--json` | On supported exits, prints one JSON summary line on stdout; suppresses some human-oriented stderr unless `--verbose`. On **`analysis`** validation (**exit code 5**), the JSON line includes **`factoryFailures`** (**SRS-57** — one object per **`failed`** factory phase) plus **`failurePhase`**, **`validationExitId`**, composed **`error`**, and **`exitMeaning`** tailored to the first failure. |
34
52
  | `--verbose` | Extra stderr logging where implemented. |
35
53
  | `--api-base-url <url>` | System Manager API origin for this process (overrides `easyspecs.apiBaseUrl` in config). |
36
54
  | `--environment production` \| `--environment staging` | Alias: **`--env`**. Overrides `easyspecs.deploymentEnvironment` for built-in URL selection when no explicit URL is set. |
package/dist/main.cjs CHANGED
@@ -10651,6 +10651,30 @@ var ExitCode = {
10651
10651
  cancelled: 8,
10652
10652
  internal: 99
10653
10653
  };
10654
+ function describeExitCode(code) {
10655
+ switch (code) {
10656
+ case ExitCode.ok:
10657
+ return "Success.";
10658
+ case ExitCode.usage:
10659
+ return "Invalid arguments, unknown command, or help was not applicable \u2014 see message above or run `easyspecs-cli help`.";
10660
+ case ExitCode.misconfiguration:
10661
+ return "Configuration or repo layout problem \u2014 fix `.easyspecs/config.json`, paths, or prerequisites, then retry.";
10662
+ case ExitCode.opencode:
10663
+ return "OpenCode (agent runner) failed \u2014 check `opencode` install, credentials, and stderr from the tool run.";
10664
+ case ExitCode.validation:
10665
+ return "Validation or factory pipeline did not succeed \u2014 see `error` above and logs for the failing phase (synthesis, coverage, remediation, index, etc.).";
10666
+ case ExitCode.auth:
10667
+ return "Authentication failed or session missing \u2014 run `easyspecs-cli auth login` or fix CI credentials in config.";
10668
+ case ExitCode.upload:
10669
+ return "Upload or cloud sync failed \u2014 check network, project id, and session; see `error` above.";
10670
+ case ExitCode.cancelled:
10671
+ return "Operation was cancelled (abort/stop).";
10672
+ case ExitCode.internal:
10673
+ return "Unexpected internal CLI error \u2014 retry; if it persists, report with full stderr and `--verbose` output.";
10674
+ default:
10675
+ return `Non-zero exit (${String(code)}) \u2014 see stderr and any JSON \`error\` field for detail.`;
10676
+ }
10677
+ }
10654
10678
 
10655
10679
  // src/cli/jsonReporter.ts
10656
10680
  function printJsonLine(envelope) {
@@ -20779,6 +20803,180 @@ var FACTORY_PIPELINE_EXIT_CONDITIONS = {
20779
20803
  backend_context_sync: "Context upload finished with no failures (quiet SRS-13 path; or cancel)."
20780
20804
  };
20781
20805
 
20806
+ // src/factory/factoryValidationFailures.ts
20807
+ function factoryFailureDisplayId(row2) {
20808
+ return row2.failureExitId;
20809
+ }
20810
+ function normalizeFactoryFailureRow(r) {
20811
+ const failureExitId = r.failureExitId ?? r.validationExitId ?? "5.0";
20812
+ const exitCode = typeof r.exitCode === "number" ? r.exitCode : ExitCode.validation;
20813
+ const factory = r.factory ?? "generate_context";
20814
+ const base = {
20815
+ factory,
20816
+ phase: r.phase,
20817
+ exitCode,
20818
+ failureExitId,
20819
+ title: r.title,
20820
+ ...typeof r.detail === "string" ? { detail: r.detail } : {},
20821
+ ...typeof r.validationSubcode === "string" ? { validationSubcode: r.validationSubcode } : {}
20822
+ };
20823
+ if (exitCode === ExitCode.validation) {
20824
+ return { ...base, validationExitId: r.validationExitId ?? failureExitId };
20825
+ }
20826
+ return base;
20827
+ }
20828
+ var FACTORY_VALIDATION_PHASE_ORDER = [
20829
+ "create_analysis_worktree",
20830
+ "materialize_opencode_agents",
20831
+ "synthesis_convergence",
20832
+ "reference_coverage",
20833
+ "zero_reference_remediation_convergence",
20834
+ "reference_coverage_execution_report",
20835
+ "link_mapping_pipeline",
20836
+ "assemble_application_context_index",
20837
+ "backend_context_sync"
20838
+ ];
20839
+ var DETAIL_MAX = 600;
20840
+ var ERROR_JOIN_MAX = 900;
20841
+ var ADDITIONAL_APPEND_MAX = 200;
20842
+ var PHASE_TO_EXIT_ID = {
20843
+ unknown_factory_phase: "5.0",
20844
+ create_analysis_worktree: "5.1",
20845
+ materialize_opencode_agents: "5.2",
20846
+ synthesis_convergence: "5.3",
20847
+ reference_coverage: "5.4",
20848
+ zero_reference_remediation_convergence: "5.5",
20849
+ reference_coverage_execution_report: "5.6",
20850
+ link_mapping_pipeline: "5.7",
20851
+ assemble_application_context_index: "5.8",
20852
+ backend_context_sync: "5.9"
20853
+ };
20854
+ var EXIT_ID_TITLES = {
20855
+ "5.0": "EasySpecs stopped: the failing pipeline phase could not be identified. Retry with --verbose; report this outcome if it persists.",
20856
+ "5.1": "Could not prepare the analysis Git worktree (temporary checkout). Check disk space, repo permissions, and that the project is a valid Git checkout.",
20857
+ "5.2": "Could not install OpenCode agent files into the analysis worktree. Check bundled extension assets and disk permissions under the worktree path.",
20858
+ "5.3": "Context synthesis did not finish: required context files are still missing or invalid after retries. Inspect OpenCode errors and workstation logs for this phase.",
20859
+ "5.4": "Reference coverage could not be computed or written. Check coverage-reference-validation.json prerequisites and disk/schema errors.",
20860
+ "5.5": "Zero-reference remediation did not complete successfully. Inspect remediation logs and zero-reference outputs for this checkout.",
20861
+ "5.6": "Reference coverage execution report could not be generated. Ensure coverage inputs exist from the coverage phase; see coverage-reference-validation.json.",
20862
+ "5.7": "Context link graph (navigation links) could not be updated. Check coordination JSON and markdown under .gluecharm/context.",
20863
+ "5.8": "Application context index could not be assembled or failed schema validation. Inspect index-application-context.json and preceding context files.",
20864
+ "5.9": "Upload / cloud sync after analysis did not succeed. Check auth, project id, network, and upload error details."
20865
+ };
20866
+ var READABLE_PHASE_LABEL = {
20867
+ create_analysis_worktree: "Analysis worktree",
20868
+ materialize_opencode_agents: "OpenCode agents",
20869
+ synthesis_convergence: "Synthesis",
20870
+ reference_coverage: "Reference coverage",
20871
+ zero_reference_remediation_convergence: "Zero-reference remediation",
20872
+ reference_coverage_execution_report: "Coverage report",
20873
+ link_mapping_pipeline: "Link graph",
20874
+ assemble_application_context_index: "Index assembly",
20875
+ backend_context_sync: "Cloud sync",
20876
+ unknown_factory_phase: "Unknown phase"
20877
+ };
20878
+ function trimDetail(s) {
20879
+ if (!s) {
20880
+ return void 0;
20881
+ }
20882
+ const t = s.trim();
20883
+ if (!t) {
20884
+ return void 0;
20885
+ }
20886
+ return t.length <= DETAIL_MAX ? t : `${t.slice(0, DETAIL_MAX)}\u2026`;
20887
+ }
20888
+ function inferValidationSubcode(phase, detail) {
20889
+ const d = detail ?? "";
20890
+ if (phase === "synthesis_convergence" && d.includes("Unstable convergence (R5)")) {
20891
+ return "R5_MACRO_PING_PONG";
20892
+ }
20893
+ if (phase === "reference_coverage" && /percentNonReferenced\s+\d/.test(d) && d.includes(">")) {
20894
+ return "COVERAGE_PERCENT_THRESHOLD";
20895
+ }
20896
+ return void 0;
20897
+ }
20898
+ function phaseToValidationExitId(phase) {
20899
+ return PHASE_TO_EXIT_ID[phase] ?? "5.0";
20900
+ }
20901
+ function titleForValidationExitId(validationExitId) {
20902
+ return EXIT_ID_TITLES[validationExitId] ?? EXIT_ID_TITLES["5.0"];
20903
+ }
20904
+ function readableLabelForFactoryPhase(phase) {
20905
+ return READABLE_PHASE_LABEL[phase] ?? phase;
20906
+ }
20907
+ function phaseIndex(key) {
20908
+ const i = FACTORY_VALIDATION_PHASE_ORDER.indexOf(key);
20909
+ return i >= 0 ? i : 999;
20910
+ }
20911
+ function buildFactoryFailuresFromRows(phases) {
20912
+ const failed = phases.filter((p) => p.status === "failed");
20913
+ failed.sort((a, b) => phaseIndex(a.key) - phaseIndex(b.key));
20914
+ return failed.map((p) => {
20915
+ const id = phaseToValidationExitId(p.key);
20916
+ const sub = inferValidationSubcode(p.key, p.detail);
20917
+ return {
20918
+ factory: "generate_context",
20919
+ phase: p.key,
20920
+ exitCode: ExitCode.validation,
20921
+ failureExitId: id,
20922
+ validationExitId: id,
20923
+ title: titleForValidationExitId(id),
20924
+ detail: trimDetail(p.detail),
20925
+ ...sub !== void 0 ? { validationSubcode: sub } : {}
20926
+ };
20927
+ });
20928
+ }
20929
+ function syntheticUnknownFactoryFailure(message) {
20930
+ return [
20931
+ {
20932
+ factory: "generate_context",
20933
+ phase: "unknown_factory_phase",
20934
+ exitCode: ExitCode.validation,
20935
+ failureExitId: "5.0",
20936
+ validationExitId: "5.0",
20937
+ title: titleForValidationExitId("5.0"),
20938
+ detail: trimDetail(message)
20939
+ }
20940
+ ];
20941
+ }
20942
+ function primaryFailureAliases(failures) {
20943
+ const last = failures[failures.length - 1];
20944
+ if (!last) {
20945
+ return { failurePhase: "unknown_factory_phase", validationExitId: "5.0" };
20946
+ }
20947
+ return {
20948
+ failurePhase: last.phase,
20949
+ validationExitId: last.validationExitId ?? factoryFailureDisplayId(last)
20950
+ };
20951
+ }
20952
+ function exitMeaningFromFactoryFailures(failures) {
20953
+ const f0 = failures[0];
20954
+ if (!f0) {
20955
+ return "";
20956
+ }
20957
+ if (failures.length === 1) {
20958
+ return f0.title;
20959
+ }
20960
+ return `${f0.title} (and ${String(failures.length - 1)} other pipeline phases failed)`;
20961
+ }
20962
+ function composeFactoryValidationError(orchestratorMessage, failures) {
20963
+ const m = orchestratorMessage?.trim() ?? "";
20964
+ if (failures.length <= 1) {
20965
+ return m.length > 0 ? m : failures[0]?.title ?? "Validation failed.";
20966
+ }
20967
+ const extraIds = failures.slice(1).map((f) => factoryFailureDisplayId(f)).join(", ");
20968
+ let appendix = `(+${String(failures.length - 1)} additional phase failure(s): ${extraIds})`;
20969
+ if (appendix.length > ADDITIONAL_APPEND_MAX) {
20970
+ appendix = appendix.slice(0, ADDITIONAL_APPEND_MAX).trimEnd() + "\u2026";
20971
+ }
20972
+ if (m.length > 0) {
20973
+ const out = `${m} | ${appendix}`;
20974
+ return out.length <= ERROR_JOIN_MAX ? out : `${m.slice(0, Math.max(0, ERROR_JOIN_MAX - appendix.length - 3))}\u2026 | ${appendix}`;
20975
+ }
20976
+ const joined = failures.map((f) => `[${factoryFailureDisplayId(f)}] ${f.title}`).join(" | ");
20977
+ return joined.length <= ERROR_JOIN_MAX ? joined : `${joined.slice(0, ERROR_JOIN_MAX - 1)}\u2026`;
20978
+ }
20979
+
20782
20980
  // src/factory/generateContextFactory.ts
20783
20981
  var FACTORY_PIPELINE_KEYS = [
20784
20982
  "create_analysis_worktree",
@@ -20886,7 +21084,11 @@ async function runGenerateContextFactory(deps) {
20886
21084
  };
20887
21085
  const fail = async (message) => {
20888
21086
  await post();
20889
- return { ok: false, message, totalElapsedMs: macroEnd() };
21087
+ let ff = buildFactoryFailuresFromRows(phases);
21088
+ if (ff.length === 0) {
21089
+ ff = syntheticUnknownFactoryFailure(message);
21090
+ }
21091
+ return { ok: false, message, totalElapsedMs: macroEnd(), factoryFailures: ff };
20890
21092
  };
20891
21093
  const pipelineCtx = {
20892
21094
  signal: deps.signal,
@@ -24348,6 +24550,155 @@ function buildFactoryDepsHeadless(input) {
24348
24550
  };
24349
24551
  }
24350
24552
 
24553
+ // src/cli/failureExitRegistry.ts
24554
+ var DRIFT_FACTORY = "context_drift";
24555
+ function failureExitIdFromParts(exitCode, minor) {
24556
+ return `${String(exitCode)}.${String(minor)}`;
24557
+ }
24558
+ var DRIFT_META = {
24559
+ INVALID_REFERENCE_PATH: {
24560
+ phase: "resolve_reference_bundle",
24561
+ exitCode: ExitCode.misconfiguration,
24562
+ minor: 1,
24563
+ title: "Reference path is not valid under the repository root or does not exist. Check the path is relative to --cwd or exists on disk."
24564
+ },
24565
+ WORKTREE_PREP_FAILED: {
24566
+ phase: "prepare_analysis_worktree",
24567
+ exitCode: ExitCode.misconfiguration,
24568
+ minor: 3,
24569
+ title: "Analysis worktree could not be prepared for drift analysis. Check git, disk space, and permissions."
24570
+ },
24571
+ EMPTY_BUNDLE: {
24572
+ phase: "resolve_reference_bundle",
24573
+ exitCode: ExitCode.validation,
24574
+ minor: 10,
24575
+ title: "Reference bundle was empty or could not be read for drift analysis."
24576
+ },
24577
+ UNRESOLVED_REFERENCE_ROOT: {
24578
+ phase: "resolve_reference_root",
24579
+ exitCode: ExitCode.validation,
24580
+ minor: 11,
24581
+ title: "Reference root document could not be resolved. Use --index or ensure an index markdown exists."
24582
+ },
24583
+ MANIFEST_FAILED: {
24584
+ phase: "build_comparison_manifest",
24585
+ exitCode: ExitCode.validation,
24586
+ minor: 12,
24587
+ title: "Comparison manifest could not be built for drift analysis."
24588
+ },
24589
+ AGENT_FAILED: {
24590
+ phase: "run_drift_analysis",
24591
+ exitCode: ExitCode.opencode,
24592
+ minor: 1,
24593
+ title: "OpenCode drift comparison did not complete successfully. Check OpenCode logs and credentials."
24594
+ },
24595
+ INVALID_AGENT_PAYLOAD: {
24596
+ phase: "validate_and_render_report",
24597
+ exitCode: ExitCode.opencode,
24598
+ minor: 2,
24599
+ title: "Drift agent returned a payload that failed validation before the report could be written."
24600
+ },
24601
+ REPORT_WRITE_FAILED: {
24602
+ phase: "validate_and_render_report",
24603
+ exitCode: ExitCode.validation,
24604
+ minor: 13,
24605
+ title: "Drift report markdown could not be written under the analysis worktree."
24606
+ },
24607
+ INDEX_PATCH_FAILED: {
24608
+ phase: "update_reference_index",
24609
+ exitCode: ExitCode.validation,
24610
+ minor: 14,
24611
+ title: "Reference root markdown could not be updated with the drift report link."
24612
+ },
24613
+ PROMOTE_FAILED: {
24614
+ phase: "promote_drift_artefacts",
24615
+ exitCode: ExitCode.upload,
24616
+ minor: 1,
24617
+ title: "Promoting drift artefacts from the analysis worktree to the workspace failed."
24618
+ }
24619
+ };
24620
+ var INDEX_PATH_ERROR_SUBSTRING = "--index";
24621
+ function invalidReferencePathMeta(error) {
24622
+ const isIndex = (error ?? "").includes(INDEX_PATH_ERROR_SUBSTRING);
24623
+ if (isIndex) {
24624
+ return {
24625
+ phase: "resolve_reference_root",
24626
+ exitCode: ExitCode.misconfiguration,
24627
+ minor: 2,
24628
+ title: "Index path (--index) is not under the repository root or does not exist. Fix the path or omit --index."
24629
+ };
24630
+ }
24631
+ return DRIFT_META.INVALID_REFERENCE_PATH;
24632
+ }
24633
+ function contextDriftExitCodeFor(code, error) {
24634
+ if (code === "INVALID_REFERENCE_PATH") {
24635
+ return invalidReferencePathMeta(error).exitCode;
24636
+ }
24637
+ return DRIFT_META[code].exitCode;
24638
+ }
24639
+ var DRIFT_PHASE_LABEL = {
24640
+ parse_cli: "CLI",
24641
+ prepare_analysis_worktree: "Analysis worktree",
24642
+ resolve_reference_bundle: "Reference bundle",
24643
+ resolve_reference_root: "Reference root",
24644
+ build_comparison_manifest: "Comparison manifest",
24645
+ run_drift_analysis: "Drift analysis",
24646
+ validate_and_render_report: "Drift report",
24647
+ update_reference_index: "Reference index",
24648
+ promote_drift_artefacts: "Promotion",
24649
+ finalize: "Finalize"
24650
+ };
24651
+ function readableLabelForDriftFactoryPhase(phase) {
24652
+ return DRIFT_PHASE_LABEL[phase] ?? phase;
24653
+ }
24654
+ function contextDriftFactoryFailureRow(code, error) {
24655
+ const meta = code === "INVALID_REFERENCE_PATH" ? invalidReferencePathMeta(error) : DRIFT_META[code];
24656
+ const failureExitId = failureExitIdFromParts(meta.exitCode, meta.minor);
24657
+ const detail = error.trim().length > 0 ? error.trim() : void 0;
24658
+ const base = {
24659
+ factory: DRIFT_FACTORY,
24660
+ phase: meta.phase,
24661
+ exitCode: meta.exitCode,
24662
+ failureExitId,
24663
+ title: meta.title,
24664
+ detail
24665
+ };
24666
+ if (meta.exitCode === ExitCode.validation) {
24667
+ return { ...base, validationExitId: failureExitId };
24668
+ }
24669
+ return base;
24670
+ }
24671
+
24672
+ // src/cli/factoryValidationStderr.ts
24673
+ function readableLabelForAnyFactory(row2) {
24674
+ return row2.factory === "context_drift" ? readableLabelForDriftFactoryPhase(row2.phase) : readableLabelForFactoryPhase(row2.phase);
24675
+ }
24676
+ function stderrLinesForFactoryFailures(failures, exitCode) {
24677
+ const lines = [`=== Easyspecs factory failure (exit ${String(exitCode)})`];
24678
+ for (const f of failures) {
24679
+ const label = readableLabelForAnyFactory(f);
24680
+ let bullet = ` \u2022 [${factoryFailureDisplayId(f)}] ${label} \u2014 ${f.title}`;
24681
+ if (f.validationSubcode) {
24682
+ bullet += ` (${f.validationSubcode})`;
24683
+ }
24684
+ lines.push(bullet);
24685
+ if (f.detail) {
24686
+ for (const part of f.detail.split(/\r?\n/)) {
24687
+ if (part.length > 0) {
24688
+ lines.push(` ${part}`);
24689
+ }
24690
+ }
24691
+ }
24692
+ }
24693
+ const n = failures.length;
24694
+ if (n === 1 && failures[0]) {
24695
+ lines.push(`Exit ${String(exitCode)} \u2014 ${failures[0].title}`);
24696
+ } else {
24697
+ lines.push(`Exit ${String(exitCode)} \u2014 ${String(n)} pipeline phase(s) failed.`);
24698
+ }
24699
+ return lines;
24700
+ }
24701
+
24351
24702
  // src/factory/updateContext/runUpdateContextFactory.ts
24352
24703
  var fs49 = __toESM(require("node:fs"));
24353
24704
  var path48 = __toESM(require("node:path"));
@@ -27260,7 +27611,23 @@ function formatCliStderrLine(line, useAnsi) {
27260
27611
  }
27261
27612
 
27262
27613
  // src/cli/main.ts
27263
- var PKG_VERSION = "0.0.14";
27614
+ var PKG_VERSION = "0.0.17";
27615
+ function isNonEmptyFactoryFailureArray(x) {
27616
+ if (!Array.isArray(x) || x.length === 0) {
27617
+ return false;
27618
+ }
27619
+ for (const item of x) {
27620
+ if (typeof item !== "object" || item === null) {
27621
+ return false;
27622
+ }
27623
+ const r = item;
27624
+ const hasId = typeof r.failureExitId === "string" || typeof r.validationExitId === "string";
27625
+ if (typeof r.phase !== "string" || typeof r.title !== "string" || !hasId) {
27626
+ return false;
27627
+ }
27628
+ }
27629
+ return true;
27630
+ }
27264
27631
  function isEasyspecsConfigReadError(e) {
27265
27632
  return e instanceof EasyspecsConfigInvalidJsonError || e instanceof EasyspecsConfigSchemaError;
27266
27633
  }
@@ -27279,6 +27646,20 @@ function logErr(flags, ...a) {
27279
27646
  console.error(...a);
27280
27647
  }
27281
27648
  }
27649
+ function logExitCodeSummary(code, flags) {
27650
+ if (code === ExitCode.ok) {
27651
+ return;
27652
+ }
27653
+ if (flags?.json === true) {
27654
+ return;
27655
+ }
27656
+ const line = `Exit ${String(code)} \u2014 ${describeExitCode(code)}`;
27657
+ if (flags) {
27658
+ logErr(flags, line);
27659
+ } else {
27660
+ console.error(line);
27661
+ }
27662
+ }
27282
27663
  function buildDoctorInspectPayload(merged, repoConfig) {
27283
27664
  return {
27284
27665
  merged: redactMergedCliSettingsForDump(merged),
@@ -27444,6 +27825,7 @@ async function main() {
27444
27825
  parsed = parseCliWithCommander(process.argv.slice(2));
27445
27826
  } catch (e) {
27446
27827
  console.error(e instanceof Error ? e.message : String(e));
27828
+ logExitCodeSummary(ExitCode.usage);
27447
27829
  process.exit(ExitCode.usage);
27448
27830
  }
27449
27831
  const { flags, positionals } = parsed;
@@ -27465,6 +27847,7 @@ async function main() {
27465
27847
  const unknown = tail.filter((t) => t !== "--overwrite");
27466
27848
  if (unknown.length > 0) {
27467
27849
  logErr(flags, `Unknown arguments for config init: ${unknown.join(", ")}`);
27850
+ logExitCodeSummary(ExitCode.usage, flags);
27468
27851
  process.exit(ExitCode.usage);
27469
27852
  }
27470
27853
  try {
@@ -27488,6 +27871,7 @@ async function main() {
27488
27871
  } catch (e) {
27489
27872
  if (isEasyspecsConfigReadError(e)) {
27490
27873
  console.error(e.message);
27874
+ logExitCodeSummary(ExitCode.misconfiguration, flags);
27491
27875
  process.exit(ExitCode.misconfiguration);
27492
27876
  }
27493
27877
  throw e;
@@ -27499,6 +27883,7 @@ async function main() {
27499
27883
  const extra = rest.slice(1).filter((t) => t.length > 0);
27500
27884
  if (!id || extra.length > 0) {
27501
27885
  logErr(flags, "Usage: easyspecs-cli config set-project-id <easyspecsProjectId>");
27886
+ logExitCodeSummary(ExitCode.usage, flags);
27502
27887
  process.exit(ExitCode.usage);
27503
27888
  }
27504
27889
  try {
@@ -27523,6 +27908,7 @@ async function main() {
27523
27908
  } catch (e) {
27524
27909
  if (isEasyspecsConfigReadError(e)) {
27525
27910
  console.error(e.message);
27911
+ logExitCodeSummary(ExitCode.misconfiguration, flags);
27526
27912
  process.exit(ExitCode.misconfiguration);
27527
27913
  }
27528
27914
  throw e;
@@ -27534,6 +27920,7 @@ async function main() {
27534
27920
  const extra = rest.slice(1).filter((t) => t.length > 0);
27535
27921
  if (!url || extra.length > 0) {
27536
27922
  logErr(flags, "Usage: easyspecs-cli config set-git-remote <url>");
27923
+ logExitCodeSummary(ExitCode.usage, flags);
27537
27924
  process.exit(ExitCode.usage);
27538
27925
  }
27539
27926
  try {
@@ -27558,6 +27945,7 @@ async function main() {
27558
27945
  } catch (e) {
27559
27946
  if (isEasyspecsConfigReadError(e)) {
27560
27947
  console.error(e.message);
27948
+ logExitCodeSummary(ExitCode.misconfiguration, flags);
27561
27949
  process.exit(ExitCode.misconfiguration);
27562
27950
  }
27563
27951
  throw e;
@@ -27576,6 +27964,7 @@ async function main() {
27576
27964
  } catch (e) {
27577
27965
  if (isEasyspecsConfigReadError(e)) {
27578
27966
  console.error(e.message);
27967
+ logExitCodeSummary(ExitCode.misconfiguration, flags);
27579
27968
  process.exit(ExitCode.misconfiguration);
27580
27969
  }
27581
27970
  throw e;
@@ -27594,12 +27983,29 @@ async function main() {
27594
27983
  repoRoot
27595
27984
  });
27596
27985
  const finish = (code, envelope) => {
27986
+ const ffRaw = envelope.factoryFailures;
27987
+ const factoryFailuresPack = isNonEmptyFactoryFailureArray(ffRaw) ? ffRaw.map(
27988
+ (r) => normalizeFactoryFailureRow(
27989
+ r
27990
+ )
27991
+ ) : void 0;
27992
+ const exitMeaningComputed = code !== ExitCode.ok ? factoryFailuresPack !== void 0 && factoryFailuresPack.length > 0 ? exitMeaningFromFactoryFailures(factoryFailuresPack) : describeExitCode(code) : "";
27597
27993
  if (flags.json) {
27598
27994
  printJsonLine({
27599
27995
  command: cmd,
27600
27996
  durationMs: Date.now() - t0,
27601
- ...envelope
27997
+ ...envelope,
27998
+ ...factoryFailuresPack !== void 0 ? { factoryFailures: factoryFailuresPack } : {},
27999
+ ...code !== ExitCode.ok ? { exitCode: code, exitMeaning: exitMeaningComputed } : {}
27602
28000
  });
28001
+ } else if (code !== ExitCode.ok) {
28002
+ if (factoryFailuresPack !== void 0 && factoryFailuresPack.length > 0) {
28003
+ for (const ln of stderrLinesForFactoryFailures(factoryFailuresPack, code)) {
28004
+ logErr(flags, ln);
28005
+ }
28006
+ } else {
28007
+ logErr(flags, `Exit ${String(code)} \u2014 ${describeExitCode(code)}`);
28008
+ }
27603
28009
  }
27604
28010
  process.exit(code);
27605
28011
  throw new Error("unreachable");
@@ -27620,6 +28026,7 @@ async function main() {
27620
28026
  } catch (e) {
27621
28027
  logErr(flags, e instanceof Error ? e.message : String(e));
27622
28028
  logErr(flags, "Usage: easyspecs-cli doctor [--readiness] [--inspect-config]");
28029
+ logExitCodeSummary(ExitCode.usage, flags);
27623
28030
  process.exit(ExitCode.usage);
27624
28031
  }
27625
28032
  const agentsDir = resolveOpenCodeAgentsDir(repoRoot, repoConfig);
@@ -27684,6 +28091,7 @@ async function main() {
27684
28091
  sessionPathRaw = extracted.sessionPath;
27685
28092
  } catch (e) {
27686
28093
  logErr(flags, e instanceof Error ? e.message : String(e));
28094
+ logExitCodeSummary(ExitCode.usage, flags);
27687
28095
  process.exit(ExitCode.usage);
27688
28096
  }
27689
28097
  const parsedCli = parseAuthLoginTail(tailForCreds);
@@ -27709,6 +28117,7 @@ async function main() {
27709
28117
  flags,
27710
28118
  "Non-interactive (--ci): set easyspecs.auth.ciLogin.email and easyspecs.auth.ciLogin.password in .easyspecs/config.json"
27711
28119
  );
28120
+ logExitCodeSummary(ExitCode.usage, flags);
27712
28121
  process.exit(ExitCode.usage);
27713
28122
  }
27714
28123
  if (sessionPathRaw !== void 0) {
@@ -28043,12 +28452,29 @@ async function main() {
28043
28452
  signal: ctrl.signal,
28044
28453
  log: (line) => logErr(flags, line)
28045
28454
  });
28046
- const driftExit = dres.exitOk && dres.ok ? ExitCode.ok : dres.ok === false && (dres.code === "INVALID_REFERENCE_PATH" || dres.code === "UNRESOLVED_REFERENCE_ROOT") ? ExitCode.usage : ExitCode.validation;
28047
- if (dres.exitOk && dres.ok && dres.driftReportPath && !flags.json && !dres.dryRun) {
28048
- console.log(dres.driftReportPath);
28455
+ if (dres.exitOk && dres.ok) {
28456
+ if (dres.driftReportPath && !flags.json && !dres.dryRun) {
28457
+ console.log(dres.driftReportPath);
28458
+ }
28459
+ const { exitOk: _driftOkA, ...driftPayload } = dres;
28460
+ finish(ExitCode.ok, driftPayload);
28461
+ }
28462
+ if (dres.ok === false) {
28463
+ const row2 = contextDriftFactoryFailureRow(dres.code, dres.error);
28464
+ const driftExit = contextDriftExitCodeFor(dres.code, dres.error);
28465
+ const { exitOk: _driftOkB, ...driftRest } = dres;
28466
+ const envelope = {
28467
+ ...driftRest,
28468
+ ok: false,
28469
+ factoryFailures: [row2],
28470
+ error: composeFactoryValidationError(dres.error, [row2])
28471
+ };
28472
+ if (driftExit === ExitCode.validation) {
28473
+ Object.assign(envelope, primaryFailureAliases([row2]));
28474
+ }
28475
+ finish(driftExit, envelope);
28049
28476
  }
28050
- const { exitOk: _driftExitOk, ...driftPayload } = dres;
28051
- finish(driftExit, driftPayload);
28477
+ finish(ExitCode.internal, { ok: false, error: "Unexpected context drift result shape." });
28052
28478
  }
28053
28479
  if (pos[0] === "analysis") {
28054
28480
  const synthesisOnly = positionals.includes("--synthesis-only");
@@ -28152,12 +28578,18 @@ async function main() {
28152
28578
  runBackendSyncImpl
28153
28579
  });
28154
28580
  const res = await runGenerateContextFactory(deps);
28155
- finish(res.ok ? ExitCode.ok : res.cancelled ? ExitCode.cancelled : ExitCode.validation, {
28581
+ const analysisEnvelope = {
28156
28582
  ok: res.ok,
28157
- error: res.message,
28158
28583
  cancelled: res.cancelled,
28159
- totalElapsedMs: res.totalElapsedMs
28160
- });
28584
+ totalElapsedMs: res.totalElapsedMs,
28585
+ error: res.message
28586
+ };
28587
+ if (!res.ok && !res.cancelled && res.factoryFailures && res.factoryFailures.length > 0) {
28588
+ analysisEnvelope.factoryFailures = res.factoryFailures;
28589
+ Object.assign(analysisEnvelope, primaryFailureAliases(res.factoryFailures));
28590
+ analysisEnvelope.error = composeFactoryValidationError(res.message, res.factoryFailures);
28591
+ }
28592
+ finish(res.ok ? ExitCode.ok : res.cancelled ? ExitCode.cancelled : ExitCode.validation, analysisEnvelope);
28161
28593
  }
28162
28594
  if (pos[0] === "update" && pos[1] === "context") {
28163
28595
  for (const a of pos.slice(2)) {
@@ -28547,10 +28979,13 @@ async function main() {
28547
28979
  command: cmd,
28548
28980
  durationMs: Date.now() - t0,
28549
28981
  ok: false,
28550
- error: e.message
28982
+ error: e.message,
28983
+ exitCode: ExitCode.misconfiguration,
28984
+ exitMeaning: describeExitCode(ExitCode.misconfiguration)
28551
28985
  });
28552
28986
  } else {
28553
28987
  console.error(e.message);
28988
+ logExitCodeSummary(ExitCode.misconfiguration, flags);
28554
28989
  }
28555
28990
  process.exit(ExitCode.misconfiguration);
28556
28991
  }