@imdeadpool/guardex 7.0.43 → 7.1.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 (63) hide show
  1. package/README.md +26 -0
  2. package/package.json +2 -1
  3. package/skills/gx-act/SKILL.md +82 -0
  4. package/src/agents/inspect.js +17 -4
  5. package/src/agents/launch.js +10 -1
  6. package/src/agents/status.js +9 -6
  7. package/src/budget/index.js +2 -1
  8. package/src/cli/args.js +52 -2
  9. package/src/cli/commands/agents.js +364 -0
  10. package/src/cli/commands/bootstrap.js +92 -0
  11. package/src/cli/commands/branch.js +127 -0
  12. package/src/cli/commands/claude.js +674 -0
  13. package/src/cli/commands/doctor.js +268 -0
  14. package/src/cli/commands/finish.js +26 -0
  15. package/src/cli/commands/mcp.js +122 -0
  16. package/src/cli/commands/misc.js +304 -0
  17. package/src/cli/commands/pr.js +439 -0
  18. package/src/cli/commands/prompt.js +92 -0
  19. package/src/cli/commands/release.js +305 -0
  20. package/src/cli/commands/report.js +244 -0
  21. package/src/cli/commands/review.js +32 -0
  22. package/src/cli/commands/setup.js +242 -0
  23. package/src/cli/commands/status.js +338 -0
  24. package/src/cli/commands/watch.js +234 -0
  25. package/src/cli/main.js +68 -3726
  26. package/src/cli/shared/repo-env.js +161 -0
  27. package/src/cli/shared/sandbox.js +417 -0
  28. package/src/cli/shared/scaffolding.js +535 -0
  29. package/src/cli/shared/toolchain-shims.js +420 -0
  30. package/src/context.js +229 -11
  31. package/src/core/runtime.js +6 -1
  32. package/src/doctor/index.js +42 -13
  33. package/src/finish/index.js +147 -5
  34. package/src/finish/preflight.js +177 -0
  35. package/src/finish/review-gate.js +182 -0
  36. package/src/git/index.js +446 -4
  37. package/src/hooks/index.js +0 -64
  38. package/src/mcp/collect.js +370 -0
  39. package/src/mcp/server.js +157 -0
  40. package/src/output/index.js +67 -1
  41. package/src/pr-review.js +23 -0
  42. package/src/pr.js +381 -0
  43. package/src/sandbox/index.js +13 -2
  44. package/src/scaffold/agent-worktree-prep.js +213 -0
  45. package/src/scaffold/index.js +108 -10
  46. package/src/speckit/index.js +226 -0
  47. package/src/terminal/index.js +1 -76
  48. package/src/terminal/tmux.js +0 -1
  49. package/src/toolchain/index.js +20 -0
  50. package/templates/AGENTS.monorepo-apps.md +26 -0
  51. package/templates/AGENTS.multiagent-safety.md +61 -347
  52. package/templates/AGENTS.multiagent-safety.min.md +11 -0
  53. package/templates/codex/skills/gx-act/SKILL.md +82 -0
  54. package/templates/githooks/pre-commit +22 -19
  55. package/templates/scripts/agent-branch-finish.sh +8 -30
  56. package/templates/scripts/agent-branch-merge.sh +4 -1
  57. package/templates/scripts/agent-branch-start.sh +88 -3
  58. package/templates/scripts/agent-preflight.sh +31 -5
  59. package/templates/scripts/agent-worktree-prune.sh +1 -1
  60. package/templates/scripts/codex-agent.sh +0 -91
  61. package/src/agents/detect.js +0 -160
  62. package/src/cockpit/keybindings.js +0 -224
  63. package/src/cockpit/layout.js +0 -224
package/src/context.js CHANGED
@@ -138,9 +138,6 @@ function toDestinationPath(relativeTemplatePath) {
138
138
  if (relativeTemplatePath.startsWith('github/')) {
139
139
  return `.${relativeTemplatePath}`;
140
140
  }
141
- if (relativeTemplatePath.startsWith('vscode/')) {
142
- return relativeTemplatePath;
143
- }
144
141
  throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
145
142
  }
146
143
 
@@ -153,7 +150,7 @@ function toDestinationPath(relativeTemplatePath) {
153
150
  // replaced with a regular file. Edit only the templates/scripts/ copy;
154
151
  // the symlink propagates.
155
152
  //
156
- // 2. SCAFFOLD-ONLY files (the 4 below + workflows + vscode extension):
153
+ // 2. SCAFFOLD-ONLY files (the 3 below + workflows):
157
154
  // tracked only under templates/; scaffolded into gitignored
158
155
  // scripts/<file> (or .githooks/<file>, etc.) by `gx setup`. Consumer
159
156
  // repos receive a regular file copy at the destination; gitguardex
@@ -165,7 +162,6 @@ function toDestinationPath(relativeTemplatePath) {
165
162
  // pattern (2), append the destination path to .gitignore's multiagent-
166
163
  // safety block (auto-managed by syncManagedGitignoreLines below).
167
164
  const TEMPLATE_FILES = [
168
- 'scripts/agent-session-state.js',
169
165
  'scripts/agent-preflight.sh',
170
166
  'scripts/guardex-docker-loader.sh',
171
167
  'scripts/guardex-env.sh',
@@ -176,9 +172,7 @@ const TEMPLATE_FILES = [
176
172
  'github/workflows/README.md',
177
173
  ];
178
174
 
179
- const PACKAGE_ROOT_SOURCE_OVERRIDES = new Set([
180
- 'scripts/agent-session-state.js',
181
- ]);
175
+ const PACKAGE_ROOT_SOURCE_OVERRIDES = new Set();
182
176
 
183
177
  const LEGACY_WORKFLOW_SHIM_SPECS = [
184
178
  { relativePath: 'scripts/agent-branch-start.sh', kind: 'shell', command: ['branch', 'start'] },
@@ -201,7 +195,6 @@ const MANAGED_TEMPLATE_SCRIPT_FILES = MANAGED_TEMPLATE_DESTINATIONS.filter((entr
201
195
 
202
196
  const LEGACY_MANAGED_REPO_FILES = [
203
197
  ...LEGACY_WORKFLOW_SHIMS,
204
- 'scripts/agent-session-state.js',
205
198
  'scripts/guardex-docker-loader.sh',
206
199
  'scripts/guardex-env.sh',
207
200
  'scripts/install-agent-git-hooks.sh',
@@ -251,7 +244,6 @@ const PACKAGE_SCRIPT_ASSETS = {
251
244
  branchMerge: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-merge.sh'),
252
245
  codexAgent: path.join(TEMPLATE_ROOT, 'scripts', 'codex-agent.sh'),
253
246
  reviewBot: path.join(TEMPLATE_ROOT, 'scripts', 'review-bot-watch.sh'),
254
- sessionState: path.join(TEMPLATE_ROOT, 'scripts', 'agent-session-state.js'),
255
247
  worktreePrune: path.join(TEMPLATE_ROOT, 'scripts', 'agent-worktree-prune.sh'),
256
248
  lockTool: path.join(TEMPLATE_ROOT, 'scripts', 'agent-file-locks.py'),
257
249
  planInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-plan-workspace.sh'),
@@ -288,6 +280,8 @@ const LOCK_FILE_RELATIVE = '.omx/state/agent-file-locks.json';
288
280
  const AGENTS_BOTS_STATE_RELATIVE = '.omx/state/agents-bots.json';
289
281
  const AGENTS_MARKER_START = '<!-- multiagent-safety:START -->';
290
282
  const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
283
+ const MONOREPO_MARKER_START = '<!-- monorepo-apps:START -->';
284
+ const MONOREPO_MARKER_END = '<!-- monorepo-apps:END -->';
291
285
  const GITIGNORE_MARKER_START = '# multiagent-safety:START';
292
286
  const GITIGNORE_MARKER_END = '# multiagent-safety:END';
293
287
  const CODEX_WORKTREE_RELATIVE_DIR = path.join('.omx', 'agent-worktrees');
@@ -315,7 +309,6 @@ const MANAGED_GITIGNORE_PATHS = [
315
309
  '!.vscode/',
316
310
  '.vscode/*',
317
311
  '!.vscode/settings.json',
318
- 'scripts/agent-session-state.js',
319
312
  'scripts/guardex-docker-loader.sh',
320
313
  'scripts/guardex-env.sh',
321
314
  '.githooks',
@@ -389,6 +382,7 @@ const SUGGESTIBLE_COMMANDS = [
389
382
  'release',
390
383
  'budget',
391
384
  'ci-init',
385
+ 'speckit',
392
386
  ];
393
387
  // CLI_COMMAND_GROUPS is the grouped source of truth the `gx --help` /
394
388
  // `gx` no-args renderer uses. Each group is ordered roughly by how often a
@@ -437,6 +431,7 @@ const CLI_COMMAND_GROUPS = [
437
431
  ['pr-review', 'Run local Codex/Claude PR review and post inline GitHub comments or write an artifact'],
438
432
  ['cockpit', 'Create or attach to a repo tmux cockpit session'],
439
433
  ['install-agent-skills', 'Install Guardex Codex/Claude skills into the user home'],
434
+ ['speckit', 'Install Spec Kit (specify-cli) SDD slash skills (/speckit-specify, /speckit-plan, ...) into the current repo'],
440
435
  ['prompt', 'Print AI setup checklist or named slices (--exec, --part, --list-parts, --snippet)'],
441
436
  ['report', 'Security/safety reports (e.g. OpenSSF scorecard, session severity)'],
442
437
  ['release', 'Create or update the current GitHub release with README-generated notes'],
@@ -700,11 +695,232 @@ const SCORECARD_RISK_BY_CHECK = {
700
695
  License: 'Low',
701
696
  };
702
697
 
698
+ // ---------------------------------------------------------------------------
699
+ // Process-scoped memoization for idempotent git/gh probes.
700
+ //
701
+ // Many gx commands (notably `gx doctor`, `gx status`, preflight checks) ask
702
+ // git/gh the same read-only questions multiple times within a single Node
703
+ // process (current branch, remote URL, worktree list, config values, PR
704
+ // state). spawnSync is cheap individually but adds up across 20+ probes.
705
+ //
706
+ // Rules:
707
+ // * Cache only idempotent reads. Strict allowlist below.
708
+ // * Never cache writes, network mutations, or anything passing stdin.
709
+ // * Lifetime = this Node process only (no disk cache, no TTL beyond the
710
+ // process). A fresh `gx ...` invocation always starts cold.
711
+ // * Honors GUARDEX_PROBE_TRACE=1 to print `[probe]` / `[probe-hit]` lines
712
+ // on stderr so duplicate calls are observable.
713
+ // * Honors GUARDEX_PROBE_CACHE=0 to disable the cache entirely
714
+ // (escape hatch).
715
+ // ---------------------------------------------------------------------------
716
+
717
+ const PROBE_CACHE = new Map();
718
+ const PROBE_TRACE_ENABLED = (() => {
719
+ const raw = String(process.env.GUARDEX_PROBE_TRACE || '').trim().toLowerCase();
720
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
721
+ })();
722
+ const PROBE_CACHE_DISABLED = (() => {
723
+ const raw = String(process.env.GUARDEX_PROBE_CACHE || '').trim().toLowerCase();
724
+ return raw === '0' || raw === 'false' || raw === 'no' || raw === 'off';
725
+ })();
726
+
727
+ // Env vars that, if they differ between calls, must invalidate cache (because
728
+ // they change git/gh's actual answer). Most env doesn't matter for read probes
729
+ // so we deliberately key only on a small slice to keep cache hits high.
730
+ const PROBE_CACHE_ENV_KEYS = [
731
+ 'GIT_DIR',
732
+ 'GIT_WORK_TREE',
733
+ 'GIT_COMMON_DIR',
734
+ 'GIT_INDEX_FILE',
735
+ 'GITHUB_TOKEN',
736
+ 'GH_TOKEN',
737
+ 'GH_HOST',
738
+ ];
739
+
740
+ function envSubsetKey(envOverride) {
741
+ const env = envOverride || process.env;
742
+ const parts = [];
743
+ for (const key of PROBE_CACHE_ENV_KEYS) {
744
+ if (env[key] !== undefined) parts.push(`${key}=${env[key]}`);
745
+ }
746
+ return parts.join('');
747
+ }
748
+
749
+ // Strip a leading `-C <path>` (or `-c key=value`) pair from git args so we
750
+ // look at the real verb after global options.
751
+ function gitVerbAndRest(args) {
752
+ if (!Array.isArray(args) || args.length === 0) return { verb: '', rest: [] };
753
+ let i = 0;
754
+ while (i < args.length) {
755
+ const a = args[i];
756
+ if (a === '-C' && i + 1 < args.length) {
757
+ i += 2;
758
+ continue;
759
+ }
760
+ if (a === '-c' && i + 1 < args.length) {
761
+ i += 2;
762
+ continue;
763
+ }
764
+ if (typeof a === 'string' && a.startsWith('-c') && a !== '-c') {
765
+ i += 1;
766
+ continue;
767
+ }
768
+ break;
769
+ }
770
+ return { verb: args[i] || '', rest: args.slice(i + 1) };
771
+ }
772
+
773
+ // A git command is cacheable only if its answer is invariant for the lifetime
774
+ // of the process under our own commands' behavior. Many git "reads"
775
+ // (`status`, `diff`, `for-each-ref`, `rev-list`, `ls-files`, `worktree list`,
776
+ // `branch --show-current`, `merge-base --is-ancestor`) can change mid-run
777
+ // because gx itself writes (commits, branch ops, worktree add/remove, config
778
+ // writes). Caching those would feed callers stale answers and break command
779
+ // flows. The narrow allowlist below intentionally covers only:
780
+ // * filesystem geometry that git itself treats as fixed for a given repo
781
+ // checkout (`rev-parse --show-toplevel|--git-common-dir|--git-dir|
782
+ // --show-cdup|--show-superproject-working-tree|--is-inside-work-tree|
783
+ // --is-bare-repository`)
784
+ // * tool version banners (`--version`)
785
+ // Everything else falls through to a real spawn each time.
786
+ function gitIsCacheableRead(args) {
787
+ const { verb, rest } = gitVerbAndRest(args);
788
+ if (!verb) return false;
789
+
790
+ if (verb === '--version' || verb === 'version') return true;
791
+
792
+ if (verb === 'rev-parse') {
793
+ // Allow ONLY the geometry-probe forms whose answer never depends on the
794
+ // working-tree, ref state, or config. Concrete ref resolution
795
+ // (`rev-parse HEAD`, `rev-parse --verify <ref>`) is intentionally NOT
796
+ // cached because gx writes refs.
797
+ for (const a of rest) {
798
+ if (typeof a !== 'string') continue;
799
+ if (a.startsWith('-')) {
800
+ if (
801
+ a === '--show-toplevel' ||
802
+ a === '--git-common-dir' ||
803
+ a === '--git-dir' ||
804
+ a === '--show-cdup' ||
805
+ a === '--show-superproject-working-tree' ||
806
+ a === '--is-inside-work-tree' ||
807
+ a === '--is-inside-git-dir' ||
808
+ a === '--is-bare-repository' ||
809
+ a === '--show-prefix'
810
+ ) {
811
+ // continue scanning to make sure no ref-probe is also present
812
+ continue;
813
+ }
814
+ // Any other flag (--verify, --short, --abbrev-ref, etc.) means we're
815
+ // resolving a ref, which is mutable.
816
+ return false;
817
+ }
818
+ // Bare positional (e.g. a ref name) -> ref resolution, mutable.
819
+ return false;
820
+ }
821
+ return rest.length > 0;
822
+ }
823
+ return false;
824
+ }
825
+
826
+ // gh probes that ARE safe to cache within a single process: only the version
827
+ // banner. Auth state can change (login/logout in another shell), PR state
828
+ // can change (a merge can land mid-poll). The instruction's allowlist
829
+ // included `gh auth status`, `gh api -X GET`, `gh pr view --json`, etc.;
830
+ // those are read-only from gh's side but their underlying truth shifts under
831
+ // gx's own writes (`gh pr create`, `gh pr merge`, `gh auth refresh`, server
832
+ // state). Within a single doctor sweep we explicitly want to re-query auth
833
+ // and PR state, so caching them would mask freshly-changed reality.
834
+ function ghIsCacheableRead(args) {
835
+ if (!Array.isArray(args) || args.length === 0) return false;
836
+ const verb = args[0] || '';
837
+ if (verb === '--version' || verb === 'version') return true;
838
+ return false;
839
+ }
840
+
841
+ function isCacheableSpawn(cmd, args, options) {
842
+ if (PROBE_CACHE_DISABLED) return false;
843
+ if (!cmd || typeof cmd !== 'string') return false;
844
+ if (options && options.input !== undefined && options.input !== null) return false;
845
+ // Pass-through if stdio is anything other than fully pipe/ignore (callers
846
+ // that inherit stdio need the real child to print, not a cached payload).
847
+ if (options && options.stdio && options.stdio !== 'pipe' && options.stdio !== 'ignore') {
848
+ if (Array.isArray(options.stdio)) {
849
+ for (const leg of options.stdio) {
850
+ if (leg && leg !== 'pipe' && leg !== 'ignore' && leg !== null) return false;
851
+ }
852
+ } else {
853
+ return false;
854
+ }
855
+ }
856
+ const base = path.basename(cmd);
857
+ if (base === 'git') return gitIsCacheableRead(args || []);
858
+ if (base === 'gh' || base === 'ghx') return ghIsCacheableRead(args || []);
859
+ if (base === 'which' || base === 'command' || base === 'type') {
860
+ return Array.isArray(args) && args.length >= 1;
861
+ }
862
+ return false;
863
+ }
864
+
865
+ function probeTrace(prefix, cmd, args) {
866
+ if (!PROBE_TRACE_ENABLED) return;
867
+ try {
868
+ process.stderr.write(`[${prefix}] ${cmd} ${(args || []).join(' ')}\n`);
869
+ } catch {
870
+ // Tracing must never break the probe.
871
+ }
872
+ }
873
+
874
+ function cachedSpawn(cmd, args, options) {
875
+ const cacheable = isCacheableSpawn(cmd, args, options);
876
+ if (!cacheable) {
877
+ probeTrace('probe', cmd, args);
878
+ return cp.spawnSync(cmd, args, options);
879
+ }
880
+ const cwdKey = (options && options.cwd) || process.cwd();
881
+ const envKey = options && options.env
882
+ ? envSubsetKey({ ...process.env, ...options.env })
883
+ : envSubsetKey(process.env);
884
+ const key = `${cmd} ${JSON.stringify(args || [])} ${cwdKey} ${envKey}`;
885
+ const cached = PROBE_CACHE.get(key);
886
+ if (cached) {
887
+ probeTrace('probe-hit', cmd, args);
888
+ // Clone so callers that mutate the result don't poison the cache.
889
+ return {
890
+ pid: cached.pid,
891
+ status: cached.status,
892
+ signal: cached.signal,
893
+ stdout: cached.stdout,
894
+ stderr: cached.stderr,
895
+ output: cached.output ? cached.output.slice() : cached.output,
896
+ error: cached.error,
897
+ };
898
+ }
899
+ probeTrace('probe', cmd, args);
900
+ const result = cp.spawnSync(cmd, args, options);
901
+ // Don't cache spawn errors (ENOENT etc.) — they may resolve on retry with a
902
+ // different binary path.
903
+ if (result && result.error) {
904
+ return result;
905
+ }
906
+ PROBE_CACHE.set(key, {
907
+ pid: result && result.pid,
908
+ status: result && result.status,
909
+ signal: result && result.signal,
910
+ stdout: result && result.stdout,
911
+ stderr: result && result.stderr,
912
+ output: result && result.output,
913
+ error: result && result.error,
914
+ });
915
+ return result;
916
+ }
917
+
703
918
  module.exports = {
704
919
  fs,
705
920
  os,
706
921
  path,
707
922
  cp,
923
+ cachedSpawn,
708
924
  PACKAGE_ROOT,
709
925
  CLI_ENTRY_PATH,
710
926
  packageJsonPath,
@@ -758,6 +974,8 @@ module.exports = {
758
974
  AGENTS_BOTS_STATE_RELATIVE,
759
975
  AGENTS_MARKER_START,
760
976
  AGENTS_MARKER_END,
977
+ MONOREPO_MARKER_START,
978
+ MONOREPO_MARKER_END,
761
979
  GITIGNORE_MARKER_START,
762
980
  GITIGNORE_MARKER_END,
763
981
  CODEX_WORKTREE_RELATIVE_DIR,
@@ -1,6 +1,7 @@
1
1
  const {
2
2
  fs,
3
3
  path,
4
+ cachedSpawn,
4
5
  CLI_ENTRY_PATH,
5
6
  PACKAGE_SCRIPT_ASSETS,
6
7
  } = require('../context');
@@ -13,8 +14,12 @@ function requireValue(rawArgs, index, flagName) {
13
14
  return value;
14
15
  }
15
16
 
17
+ // Route reads through the process-scoped probe cache. cachedSpawn caches ONLY a
18
+ // strict allowlist (git geometry probes, git/gh `--version`, `which`) and falls
19
+ // through to a real spawn for everything else — writes, ref resolution, npm,
20
+ // gh auth/pr — so observable behavior is unchanged, only redundant probes drop.
16
21
  function run(cmd, args, options = {}) {
17
- return require('node:child_process').spawnSync(cmd, args, {
22
+ return cachedSpawn(cmd, args, {
18
23
  encoding: 'utf8',
19
24
  stdio: options.stdio || 'pipe',
20
25
  cwd: options.cwd,
@@ -1,6 +1,7 @@
1
1
  const {
2
2
  fs,
3
3
  path,
4
+ cachedSpawn,
4
5
  TOOL_NAME,
5
6
  SHORT_TOOL_NAME,
6
7
  GH_BIN,
@@ -11,7 +12,23 @@ const {
11
12
  AGENT_WORKTREE_RELATIVE_DIRS,
12
13
  defaultAgentWorktreeRelativeDir,
13
14
  } = require('../context');
14
- const { run, runPackageAsset } = require('../core/runtime');
15
+ const { runPackageAsset } = require('../core/runtime');
16
+
17
+ // Route doctor probe-running calls through the process-scoped probe cache.
18
+ // cachedSpawn falls through to cp.spawnSync for any non-allowlisted call
19
+ // (git commit/push/stash/checkout, gh auth login, etc.), so writes are never
20
+ // cached. Doctor fires the same read questions many times within one run
21
+ // (current branch, remote URL, worktree list, gh auth status) — caching
22
+ // those is a pure perf win.
23
+ function run(cmd, args, options = {}) {
24
+ return cachedSpawn(cmd, args, {
25
+ encoding: 'utf8',
26
+ stdio: options.stdio || 'pipe',
27
+ cwd: options.cwd,
28
+ env: options.env ? { ...process.env, ...options.env } : process.env,
29
+ timeout: options.timeout,
30
+ });
31
+ }
15
32
  const {
16
33
  currentBranchName,
17
34
  gitRefExists,
@@ -31,7 +48,7 @@ const {
31
48
  cleanupProtectedBaseSandbox,
32
49
  } = require('../sandbox');
33
50
  const { ensureOmxScaffold, configureHooks } = require('../scaffold');
34
- const { detectRecoverableAutoFinishConflict, printAutoFinishSummary } = require('../output');
51
+ const { detectRecoverableAutoFinishConflict, printAutoFinishSummary, isTerseMode } = require('../output');
35
52
  const { autoCommitWorktreeForFinish } = require('../finish');
36
53
 
37
54
  /**
@@ -101,6 +118,7 @@ function buildSandboxDoctorArgs(options, sandboxTarget) {
101
118
  if (options.skipAgents) args.push('--skip-agents');
102
119
  if (options.skipPackageJson) args.push('--skip-package-json');
103
120
  if (options.skipGitignore) args.push('--no-gitignore');
121
+ if (options.contract) args.push('--contract');
104
122
  if (!options.dropStaleLocks) args.push('--keep-stale-locks');
105
123
  args.push(options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge');
106
124
  if (options.verboseAutoFinish) args.push('--verbose-auto-finish');
@@ -1152,6 +1170,7 @@ function emitDoctorSandboxJsonOutput(nestedResult, execution) {
1152
1170
  }
1153
1171
 
1154
1172
  function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult, nestedResult, execution) {
1173
+ const terse = isTerseMode();
1155
1174
  console.log(
1156
1175
  `[${TOOL_NAME}] doctor detected protected branch '${blocked.branch}'. ` +
1157
1176
  `Running repairs in sandbox branch '${metadata.branch || 'agent/<auto>'}'.`,
@@ -1164,6 +1183,10 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1164
1183
  return;
1165
1184
  }
1166
1185
 
1186
+ // Terse mode: drop "[OK] X skipped because of Y" / "already in sync"
1187
+ // confirmations. Keep committed/failed/pending/merged states verbose so
1188
+ // operators still see action-required hints, PR URLs, branch names, and
1189
+ // file paths.
1167
1190
  if (execution.autoCommit.status === 'committed') {
1168
1191
  console.log(
1169
1192
  `[${TOOL_NAME}] Auto-committed doctor repairs in sandbox branch '${metadata.branch}'.`,
@@ -1172,22 +1195,24 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1172
1195
  console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit failed; branch left for manual follow-up.`);
1173
1196
  if (execution.autoCommit.stdout) process.stdout.write(execution.autoCommit.stdout);
1174
1197
  if (execution.autoCommit.stderr) process.stderr.write(execution.autoCommit.stderr);
1175
- } else {
1198
+ } else if (!terse) {
1176
1199
  console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit skipped: ${execution.autoCommit.note}.`);
1177
1200
  }
1178
1201
 
1179
1202
  if (execution.protectedBaseRepairSync.status === 'merged') {
1180
1203
  console.log(`[${TOOL_NAME}] Fast-forwarded tracked doctor repairs into the protected branch workspace.`);
1181
- } else if (execution.protectedBaseRepairSync.status === 'unchanged') {
1182
- console.log(`[${TOOL_NAME}] Protected branch workspace already had the tracked doctor repairs.`);
1183
1204
  } else if (execution.protectedBaseRepairSync.status === 'would-merge') {
1184
1205
  console.log(`[${TOOL_NAME}] Dry run: would fast-forward tracked doctor repairs into the protected branch workspace.`);
1185
1206
  } else if (execution.protectedBaseRepairSync.status === 'failed') {
1186
1207
  console.log(`[${TOOL_NAME}] Protected branch tracked repair merge failed: ${execution.protectedBaseRepairSync.note}.`);
1187
1208
  if (execution.protectedBaseRepairSync.stdout) process.stdout.write(execution.protectedBaseRepairSync.stdout);
1188
1209
  if (execution.protectedBaseRepairSync.stderr) process.stderr.write(execution.protectedBaseRepairSync.stderr);
1189
- } else {
1190
- console.log(`[${TOOL_NAME}] Protected branch tracked repair merge skipped: ${execution.protectedBaseRepairSync.note}.`);
1210
+ } else if (!terse) {
1211
+ if (execution.protectedBaseRepairSync.status === 'unchanged') {
1212
+ console.log(`[${TOOL_NAME}] Protected branch workspace already had the tracked doctor repairs.`);
1213
+ } else {
1214
+ console.log(`[${TOOL_NAME}] Protected branch tracked repair merge skipped: ${execution.protectedBaseRepairSync.note}.`);
1215
+ }
1191
1216
  }
1192
1217
 
1193
1218
  if (execution.lockSync.status === 'synced') {
@@ -1195,8 +1220,10 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1195
1220
  `[${TOOL_NAME}] Synced repaired lock registry back to protected branch workspace (${LOCK_FILE_RELATIVE}).`,
1196
1221
  );
1197
1222
  } else if (execution.lockSync.status === 'unchanged') {
1223
+ // Kept verbose in terse mode too: downstream consumers (and tests) rely
1224
+ // on seeing the lock-registry sync stage reach a terminal state line.
1198
1225
  console.log(`[${TOOL_NAME}] Lock registry already synced in protected branch workspace.`);
1199
- } else {
1226
+ } else if (!terse) {
1200
1227
  console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${execution.lockSync.note}.`);
1201
1228
  }
1202
1229
 
@@ -1217,7 +1244,7 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1217
1244
  console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
1218
1245
  if (execution.finish.stdout) process.stdout.write(execution.finish.stdout);
1219
1246
  if (execution.finish.stderr) process.stderr.write(execution.finish.stderr);
1220
- } else {
1247
+ } else if (!terse) {
1221
1248
  console.log(`[${TOOL_NAME}] Auto-finish skipped: ${execution.finish.note}.`);
1222
1249
  }
1223
1250
 
@@ -1227,12 +1254,14 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1227
1254
  });
1228
1255
  if (execution.omxScaffoldSync.status === 'synced') {
1229
1256
  console.log(`[${TOOL_NAME}] Synced .omx scaffold back to protected branch workspace.`);
1230
- } else if (execution.omxScaffoldSync.status === 'unchanged') {
1231
- console.log(`[${TOOL_NAME}] .omx scaffold already aligned in protected branch workspace.`);
1232
1257
  } else if (execution.omxScaffoldSync.status === 'would-sync') {
1233
1258
  console.log(`[${TOOL_NAME}] Dry run: would sync .omx scaffold back to protected branch workspace.`);
1234
- } else {
1235
- console.log(`[${TOOL_NAME}] .omx scaffold sync skipped: ${execution.omxScaffoldSync.note}.`);
1259
+ } else if (!terse) {
1260
+ if (execution.omxScaffoldSync.status === 'unchanged') {
1261
+ console.log(`[${TOOL_NAME}] .omx scaffold already aligned in protected branch workspace.`);
1262
+ } else {
1263
+ console.log(`[${TOOL_NAME}] .omx scaffold sync skipped: ${execution.omxScaffoldSync.note}.`);
1264
+ }
1236
1265
  }
1237
1266
  }
1238
1267