mandrel 2.9.0 → 2.11.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.
Files changed (75) hide show
  1. package/.agents/agents/.markdownlint.json +4 -0
  2. package/.agents/agents/acceptance-critic.md +30 -5
  3. package/.agents/agents/auditor.md +36 -19
  4. package/.agents/agents/plan-critic.md +31 -5
  5. package/.agents/agents/story-worker.md +91 -100
  6. package/.agents/docs/configuration.md +39 -25
  7. package/.agents/docs/execution-reference.md +13 -0
  8. package/.agents/docs/workflows.md +1 -1
  9. package/.agents/instructions.md +131 -265
  10. package/.agents/rules/git-conventions.md +47 -83
  11. package/.agents/rules/orchestration-error-handling.md +28 -0
  12. package/.agents/schemas/agentrc.schema.json +36 -9
  13. package/.agents/schemas/validation-evidence.schema.json +3 -1
  14. package/.agents/scripts/acceptance-eval.js +10 -6
  15. package/.agents/scripts/apply-quality-bootstrap.js +1 -1
  16. package/.agents/scripts/check-test-temp-hygiene.js +438 -0
  17. package/.agents/scripts/deliver-recover.js +23 -6
  18. package/.agents/scripts/lib/audit-suite/index.js +5 -0
  19. package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +179 -0
  20. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  21. package/.agents/scripts/lib/baselines/env-overrides.js +33 -0
  22. package/.agents/scripts/lib/baselines/git-base.js +0 -0
  23. package/.agents/scripts/lib/baselines/preview-gates.js +5 -0
  24. package/.agents/scripts/lib/config/gates/maintainability.schema.js +10 -1
  25. package/.agents/scripts/lib/config/quality.js +13 -0
  26. package/.agents/scripts/lib/config/temp-paths.js +121 -1
  27. package/.agents/scripts/lib/config-settings-schema-delivery.js +30 -0
  28. package/.agents/scripts/lib/config-settings-schema.js +12 -16
  29. package/.agents/scripts/lib/observability/metrics-ledger.js +217 -0
  30. package/.agents/scripts/lib/observability/runtime-friction.js +7 -0
  31. package/.agents/scripts/lib/orchestration/ceremony-routing.js +45 -0
  32. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +97 -4
  33. package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
  34. package/.agents/scripts/lib/orchestration/complexity-gate.js +533 -93
  35. package/.agents/scripts/lib/orchestration/deliver-recover.js +137 -10
  36. package/.agents/scripts/lib/orchestration/merge-block-class.js +36 -15
  37. package/.agents/scripts/lib/orchestration/merge-poll.js +213 -0
  38. package/.agents/scripts/lib/orchestration/plan-context.js +119 -3
  39. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +182 -9
  40. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +29 -2
  41. package/.agents/scripts/lib/orchestration/plan-metrics.js +31 -82
  42. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +153 -2
  43. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +221 -14
  44. package/.agents/scripts/lib/orchestration/resolve-stories.js +12 -1
  45. package/.agents/scripts/lib/orchestration/review-depth.js +9 -4
  46. package/.agents/scripts/lib/orchestration/review-providers/native.js +34 -16
  47. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -3
  48. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +230 -79
  49. package/.agents/scripts/lib/orchestration/spec-budget.js +78 -0
  50. package/.agents/scripts/lib/orchestration/story-body-gate.js +72 -0
  51. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +89 -1
  52. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +73 -0
  53. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +6 -0
  54. package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -62
  55. package/.agents/scripts/lib/templates/decomposer-prompts.js +13 -6
  56. package/.agents/scripts/lib/test-env.js +65 -0
  57. package/.agents/scripts/plan-context.js +84 -9
  58. package/.agents/scripts/plan-critics.js +115 -3
  59. package/.agents/scripts/plan-persist.js +11 -1
  60. package/.agents/scripts/plan-run-epilogue.js +1 -1
  61. package/.agents/scripts/resolve-stories.js +2 -0
  62. package/.agents/scripts/single-story-confirm-merge.js +65 -5
  63. package/.agents/scripts/stories-wave-tick.js +1 -1
  64. package/.agents/workflows/deliver.md +88 -230
  65. package/.agents/workflows/helpers/acceptance-self-eval.md +16 -5
  66. package/.agents/workflows/helpers/deliver-reference.md +171 -0
  67. package/.agents/workflows/helpers/deliver-story-reference.md +223 -0
  68. package/.agents/workflows/helpers/deliver-story.md +115 -432
  69. package/.agents/workflows/helpers/plan-reference.md +246 -0
  70. package/.agents/workflows/plan.md +108 -304
  71. package/docs/CHANGELOG.md +46 -0
  72. package/lib/cli/registry.js +31 -14
  73. package/lib/migrations/index.js +2 -0
  74. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +92 -0
  75. package/package.json +1 -1
@@ -1,99 +1,63 @@
1
1
  # Git & Version Control Conventions
2
2
 
3
- This rule applies globally to all repository changes to maintain a clean git
4
- history. It is the **always-on core**: branch shapes, the Conventional-Commit
5
- subject contract, issue references, and the non-negotiable push/hygiene MUSTs.
6
-
7
- Deeper mechanics the hard-cutover contract policy, the push-hook
8
- false-negative signature, shared-checkout merge contention, the
9
- documentation-freshness gate, and the `meta::*` retrospective-routing labels —
10
- live in the on-demand
11
- [`git-conventions-reference.md`](git-conventions-reference.md). Read that file
12
- **before** doing the matching work; the core below is sufficient for every
13
- routine commit, branch, and PR.
3
+ This rule applies globally to all repository changes. It is the **always-on
4
+ core**: branch shapes, the Conventional-Commit subject contract, issue
5
+ references, and the non-negotiable push/hygiene MUSTs. Deeper mechanics —
6
+ the hard-cutover contract policy, the full push-validation procedure and
7
+ the push-hook false-negative signature, checkout-hygiene scope rules,
8
+ shared-checkout merge contention, the documentation-freshness gate, and
9
+ the `meta::*` labels — live in the on-demand
10
+ [`git-conventions-reference.md`](git-conventions-reference.md); read it
11
+ **before** doing the matching work.
14
12
 
15
13
  ## Canonical Branching (v2)
16
14
 
17
- ### Story branch PR main
18
-
19
- Every Story lands on a dedicated **Story branch** named `story-<storyId>`
20
- (e.g., `story-104`), seeded from `project.baseBranch` (`main` by default).
21
- The runtime owns branch creation via `single-story-init.js`; agents commit
22
- on that branch only. Close opens a PR against `main` (squash + required
23
- checks). There is no `epic/<id>` integration branch and no `--no-ff`
24
- wave merge.
25
-
26
- > **Commit subjects.** Stories have no child tickets. Commits land on
27
- > `story-<storyId>` directly from the agent and the Conventional Commit
28
- > subject references the Story via `(refs #<storyId>)`. See
29
- > [`.agents/instructions.md` § 5.D](../instructions.md) for the hierarchy
30
- > contract.
15
+ Every Story lands on a dedicated **Story branch** named
16
+ `story-<storyId>`, seeded from `project.baseBranch` (`main` by default).
17
+ The runtime owns branch creation via `single-story-init.js`; agents
18
+ commit on that branch only. Close opens a PR against `main` (squash +
19
+ required checks). No `epic/<id>` integration branch, no `--no-ff` wave
20
+ merge, no child tickets: commits land on `story-<storyId>` directly, the
21
+ subject referencing the Story via `(refs #<storyId>)` see
22
+ [`.agents/instructions.md` § 5.D](../instructions.md).
31
23
 
32
24
  ## Conventional Commits
33
25
 
34
- - MUST adhere to Conventional Commits format:
35
- `<type>(<optional scope>): <description>`
36
- - Types allowed: `feat:`, `fix:`, `perf:`, `refactor:`, `revert:`, `docs:`,
37
- `style:`, `chore:`, `test:`, `build:`, `ci:`. This list mirrors the
38
- `changelog-sections` in `release-please-config.json`; keep the two in
39
- sync when adding a type.
40
- - Description must be in the imperative mood (e.g., "add feature", not
41
- "adds" or "added").
42
- - **Local enforcement**: the `commit-msg` Husky hook runs `commitlint`
43
- against every local commit (`.husky/commit-msg` →
44
- `commitlint --edit "$1"`, config in `commitlint.config.js`). A
45
- non-conventional subject fails the hook and no commit is created. Do not
46
- bypass with `--no-verify`. The hook does **not** run on squash-merge
47
- titles edited in the GitHub UI; author the PR title in conventional form
48
- so the squash commit on `main` parses cleanly for release-please.
49
-
50
- ## Push Validation & Reliability
51
-
52
- To prevent "silent" push failures (e.g., hidden by multi-command chains or
53
- rejected by `pre-push` hooks):
54
-
55
- 1. **Local Validation**: Run the project's configured validation commands
56
- (`agentSettings.commands.validate` and `agentSettings.commands.test` in
57
- `.agentrc.json`, or the equivalent format-check command) locally
58
- _before_ attempting a `git push`.
59
- 2. **Verify Push Output**: Do NOT assume a push succeeded unless the output
60
- explicitly confirms the remote ref was updated (`[new branch]`,
61
- `[up to date]`, or `... -> ...`).
62
- 3. **Handle Rejections**: If a push is rejected by a `pre-push` hook, fix
63
- the underlying issue (usually formatting or linting) and create a NEW
64
- follow-up commit. Do **not** amend the rejected commit — amending makes
65
- diffs harder to review and can lose work if the original commit
66
- contained more than the linting fix.
67
- 4. **Never bypass hooks**: Do not use `--no-verify`, `--no-gpg-sign`, or
68
- other hook-skipping flags unless the operator explicitly authorizes it.
69
- If a hook fails, investigate the underlying cause. One recognized
70
- exception — a Biome _zero-match_ failure under a harness-managed
71
- worktree path the consumer's lint config ignores — is a consumer-tooling
72
- gap, **not** authorization to skip the hook; its recognition signature
73
- and sanctioned fix live in
74
- [`git-conventions-reference.md` § Push Validation](git-conventions-reference.md).
26
+ - MUST follow `<type>(<optional scope>): <description>`, imperative
27
+ mood.
28
+ - Types: `feat:`, `fix:`, `perf:`, `refactor:`, `revert:`, `docs:`,
29
+ `style:`, `chore:`, `test:`, `build:`, `ci:` mirrors
30
+ `changelog-sections` in `release-please-config.json`; keep in sync.
31
+ - The `commit-msg` Husky hook runs `commitlint` on every local commit.
32
+ It does **not** run on squash-merge titles edited in the GitHub UI —
33
+ author the PR title in conventional form so the squash commit parses
34
+ for release-please.
35
+
36
+ ## Push Validation & Reliability (MUSTs)
37
+
38
+ 1. Run the configured validation commands locally **before** `git push`.
39
+ 2. Do NOT assume a push succeeded unless the output confirms the remote
40
+ ref was updated (`[new branch]`, `[up to date]`, `... -> ...`).
41
+ 3. If a `pre-push` hook rejects, fix the cause and create a NEW follow-up
42
+ commit never amend the rejected commit.
43
+ 4. **Never bypass hooks** (`--no-verify`, `--no-gpg-sign`, …) without
44
+ explicit operator authorization. The one recognized exception a
45
+ Biome zero-match failure under a harness-managed worktree path — is a
46
+ consumer-tooling gap, **not** authorization; see
47
+ [`git-conventions-reference.md` § Push Validation](git-conventions-reference.md).
75
48
 
76
49
  ## Local checkout hygiene
77
50
 
78
- **Invariant: the delivering flow owns tidying the local checkout — reaping its
79
- own merged refs and fast-forwarding the base branch. `/git-cleanup` is a
80
- recovery tool, not a routine chore.**
81
-
82
- Every flow that lands work `/deliver` (Epic and standalone-Story paths),
83
- `/git-deliver` — leaves the local checkout tidy without operator
84
- intervention: it fast-forwards the base branch itself, and reaps its own
85
- merged local refs on the next workflow boot (the `boot-sweep.js` protected
86
- sweep). Do **not** end a workflow by telling the operator to run
87
- `/git-cleanup` to catch up; `/git-cleanup` is for recovering an unusual state
88
- the automated hygiene does not cover. The boot-sweep scope rules, the
89
- content-merged report-only case, and the shared-checkout merge-contention
90
- guard are detailed in
51
+ **The delivering flow owns tidying the local checkout**it
52
+ fast-forwards the base branch itself and reaps its own merged refs on
53
+ the next workflow boot (the `boot-sweep.js` protected sweep).
54
+ `/git-cleanup` is a recovery tool, not a routine chore — never end a
55
+ workflow by telling the operator to run it. Scope rules and the
56
+ shared-checkout contention guard:
91
57
  [`git-conventions-reference.md` § Local checkout hygiene](git-conventions-reference.md).
92
58
 
93
59
  ## Pull Requests
94
60
 
95
61
  - Never commit `.env` or hardcoded secrets.
96
- - Always include a short description of _why_ the change was made in the PR
97
- body.
98
- - **Reference Issues**: Use "Resolves #109" or "Closes #114" to link
99
- tickets.
62
+ - Always include a short description of _why_ the change was made.
63
+ - Reference issues: "Resolves #109" / "Closes #114".
@@ -23,3 +23,31 @@ stubbed, letting execution continue past the intended hard-stop.
23
23
  Non-orchestration scripts (one-shot utilities, audit reporters, doc
24
24
  generators) may continue to use `Logger.fatal` where the lifetime guarantees
25
25
  are simpler.
26
+
27
+ ## Output Contract — compact digest + artifact path (Story #4708)
28
+
29
+ An orchestration script's stdout rides resident in the invoking agent's
30
+ transcript and is re-read as prompt-cache input on every later turn, so fat
31
+ success output is a per-turn tax on the whole session — not a one-time cost.
32
+
33
+ - **Budget.** A script's **default success-path stdout MUST stay under
34
+ ~2KB**. Error paths and opt-in verbose modes are exempt; the budget is
35
+ about what every routine invocation pays.
36
+ - **Digest + artifact path.** When a script produces a large result (a full
37
+ envelope, a per-phase result object, a report), it MUST write the full
38
+ payload to an on-disk artifact (under `temp/`, or an operator-named
39
+ `--out` path) and emit a **compact machine-parseable digest** on stdout
40
+ that names the artifact path plus the handful of fields the caller acts
41
+ on. The shared helper
42
+ [`lib/observability/terse-result.js`](../scripts/lib/observability/terse-result.js)
43
+ (`emitTerseResult`, Story #4685) implements this for hot-path result
44
+ dumps; `plan-context.js --out` shows the envelope-capture variant.
45
+ - **Compact JSON.** Machine-parsed stdout envelopes are `JSON.parse`d by
46
+ their drivers — emit them single-line (`JSON.stringify(x)`), never
47
+ pretty-printed (`null, 2` only adds resident bytes). Pretty output is
48
+ reserved for explicit opt-in flags (`--pretty`).
49
+ - **Escape hatch.** `MANDREL_RESULT_DETAIL=inline` restores inline full
50
+ detail for interactive debugging; scripts using `emitTerseResult` honor it
51
+ automatically.
52
+ - **stdout purity is unchanged.** Scripts whose stdout is a machine contract
53
+ (Story #2278) keep logs on stderr; the digest is the *only* stdout line.
@@ -329,21 +329,16 @@
329
329
  },
330
330
  "complexityGate": {
331
331
  "type": "object",
332
- "description": "Plan-time ceremony-lite complexity gate. Routes trivial single-artifact seeds onto a collapsed plan/deliver path; conservative (full on any doubt). Never relaxes the Story-ticket / PR-to-main / repo-gates / security-baseline non-negotiables.",
332
+ "description": "Shape-derived ceremony-lite complexity routing. A lite claim is validated against the authored Story shape at persist and re-derived from the Story body at dispatch; conservative (full on any doubt). Never relaxes the Story-ticket / PR-to-main / repo-gates / security-baseline non-negotiables.",
333
333
  "properties": {
334
334
  "enabled": {
335
335
  "type": "boolean",
336
- "description": "Master switch. When false, every seed takes the full plan/deliver ceremony. Default true."
337
- },
338
- "maxSeedWords": {
339
- "type": "integer",
340
- "minimum": 0,
341
- "description": "Seed prose word ceiling for the lite path. A seed above this many words is not trivial and takes the full path. Default 60."
336
+ "description": "Master switch. When false, lite routing is disabled everywhere: persist refuses lite claims and dispatch always takes the sub-agent path. Default true."
342
337
  },
343
338
  "maxArtifacts": {
344
339
  "type": "integer",
345
340
  "minimum": 0,
346
- "description": "Enumerated-artifact ceiling for the lite path. A seed enumerating more than this many candidate artifacts is multi-capability and takes the full path. Default 1."
341
+ "description": "Enumerated-artifact threshold reported by the plan-context complexity signals. An input signal for the planner verdict carries no routing authority. Default 1."
347
342
  }
348
343
  },
349
344
  "additionalProperties": false
@@ -844,6 +839,16 @@
844
839
  "$ref": "#/$defs/listOrExtenderOfStrings",
845
840
  "description": "Directories whose JS sources the maintainability gate scores. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/` plus `tests/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal."
846
841
  },
842
+ "refreshTag": {
843
+ "allOf": [
844
+ {
845
+ "$ref": "#/$defs/safeString"
846
+ },
847
+ {
848
+ "minLength": 1
849
+ }
850
+ ]
851
+ },
847
852
  "refreshTimeoutMs": {
848
853
  "type": "integer",
849
854
  "minimum": 1,
@@ -1171,8 +1176,14 @@
1171
1176
  },
1172
1177
  "mergeWatch": {
1173
1178
  "type": "object",
1174
- "description": "Knobs consumed by the close-and-land merge wait (Story #4543; defaults in `lib/orchestration/merge-poll.js`). `intervalSeconds` is the poll cadence between `gh pr view` probes after the arm. `maxWaitSeconds` bounds ONE invocation of the merge wait and its expiry returns a resumable `pending` terminal with no label mutation; `maxBudgetSeconds` bounds the CUMULATIVE wait across resumes (anchored at the PR's createdAt, so a resume does not restart the clock) and exhausting it is the genuine give-up that classifies and blocks. `updateAttempts` caps the bounded update of a behind-the-base PR.",
1179
+ "description": "Knobs consumed by the close-and-land merge wait (Story #4543; defaults in `lib/orchestration/merge-poll.js`). `mode` (Story #4698) selects the close-time merge posture. `intervalSeconds` is the poll cadence between `gh pr view` probes after the arm. `maxWaitSeconds` bounds ONE invocation of the merge wait and its expiry returns a resumable `pending` terminal with no label mutation; `maxBudgetSeconds` bounds the CUMULATIVE wait across resumes (anchored at the PR's createdAt, so a resume does not restart the clock) and exhausting it is the genuine give-up that classifies and blocks. `updateAttempts` caps the bounded update of a behind-the-base PR.",
1175
1180
  "properties": {
1181
+ "mode": {
1182
+ "type": "string",
1183
+ "enum": ["sync", "async"],
1184
+ "default": "sync",
1185
+ "description": "Close-time merge-wait posture (Story #4698). `sync` (default) keeps the in-close foreground merge wait unchanged. `async` caps the per-invocation wait to a short ~60s probe window — long enough to catch an instant merge and, via the head-anchored required-check predicate, an instantly-red required check — then returns the resumable `pending` terminal (exit 3) with a `nextCommand`. Opt in when slow CI makes the foreground wait routinely expire: the worker launches `nextCommand` in the background instead of burning the host tool slot polling. `maxBudgetSeconds` (the cumulative give-up) is unchanged."
1186
+ },
1176
1187
  "intervalSeconds": {
1177
1188
  "type": "integer",
1178
1189
  "minimum": 1,
@@ -1200,6 +1211,19 @@
1200
1211
  },
1201
1212
  "additionalProperties": false
1202
1213
  },
1214
+ "review": {
1215
+ "type": "object",
1216
+ "description": "Close-scope review tuning (Story #4699). Governs the Story-scope local-lens pass that runs inside the close subprocess; the maker-blind code-review pass and all hard gates are unaffected.",
1217
+ "properties": {
1218
+ "lensDiffFloor": {
1219
+ "type": "integer",
1220
+ "minimum": 0,
1221
+ "default": 40,
1222
+ "description": "Changed-line floor for the close-scope lens walk (Story #4699). A diff strictly below this many changed lines (additions + deletions) with zero sensitive-path hits skips lens materialization and records the skip in the findings-yield ledger. Default 40; 0 disables the skip."
1223
+ }
1224
+ },
1225
+ "additionalProperties": false
1226
+ },
1203
1227
  "quality": {
1204
1228
  "type": "object",
1205
1229
  "properties": {
@@ -1312,6 +1336,9 @@
1312
1336
  "codeReview": {
1313
1337
  "$ref": "#/$defs/codeReview"
1314
1338
  },
1339
+ "review": {
1340
+ "$ref": "#/$defs/review"
1341
+ },
1315
1342
  "refactorStage": {
1316
1343
  "$ref": "#/$defs/refactorStage"
1317
1344
  },
@@ -36,10 +36,12 @@
36
36
  "lint",
37
37
  "test",
38
38
  "format",
39
+ "coverage-capture",
40
+ "check-baselines",
39
41
  "check-maintainability",
40
42
  "check-crap"
41
43
  ],
42
- "description": "Stable gate identifier. Closed enum — additions require a schema bump."
44
+ "description": "Stable gate identifier. Closed enum — additions require a schema bump. Must be a superset of every gate name buildDefaultGates() can emit (lib/close-validation/gates.js); tests/close-validation-gates-enum.test.js pins that gate-list ⊆ enum invariant. `coverage-capture` and `check-baselines` are the real close-validation gates (Story #4697); `check-maintainability` / `check-crap` are the retired per-kind gates (Story #2210) kept so historical evidence records still validate."
43
45
  },
44
46
  "commitSha": {
45
47
  "type": "string",
@@ -3,12 +3,16 @@
3
3
  /**
4
4
  * acceptance-eval.js — bounded per-Story acceptance self-eval gate (Story #3819).
5
5
  *
6
- * The Story-implementation phase runs an independent (fresh-context)
7
- * critic pass that scores the caller-injected change set against each
8
- * inline `acceptance[]` item, emitting one verdict file per round
6
+ * The Story-implementation phase runs ONE verdict-owner per acceptance
7
+ * cluster (Story #4723) the fresh-context critic when
8
+ * `ceremony-routing.js` sensitivity-routes the cluster fresh, the
9
+ * contract-identical inline self-eval otherwise — which scores the
10
+ * caller-injected change set against each inline `acceptance[]` item and
11
+ * emits one verdict file per round
9
12
  * (`.agents/schemas/acceptance-eval-verdict.schema.json`). This CLI is the
10
- * deterministic substrate that turns that verdict into the loop's next
11
- * action:
13
+ * deterministic SCORER of that single authored verdict it validates and
14
+ * decides, it never re-scores the criteria as an independent additional
15
+ * pass — turning the verdict into the loop's next action:
12
16
  *
13
17
  * 1. Validate the verdict file against the verdict JSON Schema (a
14
18
  * malformed verdict is a hard error — the loop refuses to guess).
@@ -283,7 +287,7 @@ export async function main(argv = process.argv.slice(2)) {
283
287
  emitSignal,
284
288
  });
285
289
 
286
- Logger.info(JSON.stringify(envelope, null, 2));
290
+ Logger.info(JSON.stringify(envelope));
287
291
 
288
292
  if (exitCode !== 0) {
289
293
  const names = envelope.unmetCriteria
@@ -69,7 +69,7 @@ async function main() {
69
69
  // Mirror the retired heredoc's output: pretty-printed `{ quality, baselines }`
70
70
  // to stdout. Use process.stdout.write (not console.log) per the no-console
71
71
  // enforcement boundary.
72
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
72
+ process.stdout.write(`${JSON.stringify(result)}\n`);
73
73
  return 0;
74
74
  }
75
75