@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.
- package/README.md +26 -0
- package/package.json +2 -1
- package/skills/gx-act/SKILL.md +82 -0
- package/src/agents/inspect.js +17 -4
- package/src/agents/launch.js +10 -1
- package/src/agents/status.js +9 -6
- package/src/budget/index.js +2 -1
- package/src/cli/args.js +52 -2
- package/src/cli/commands/agents.js +364 -0
- package/src/cli/commands/bootstrap.js +92 -0
- package/src/cli/commands/branch.js +127 -0
- package/src/cli/commands/claude.js +674 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/finish.js +26 -0
- package/src/cli/commands/mcp.js +122 -0
- package/src/cli/commands/misc.js +304 -0
- package/src/cli/commands/pr.js +439 -0
- package/src/cli/commands/prompt.js +92 -0
- package/src/cli/commands/release.js +305 -0
- package/src/cli/commands/report.js +244 -0
- package/src/cli/commands/review.js +32 -0
- package/src/cli/commands/setup.js +242 -0
- package/src/cli/commands/status.js +338 -0
- package/src/cli/commands/watch.js +234 -0
- package/src/cli/main.js +68 -3726
- package/src/cli/shared/repo-env.js +161 -0
- package/src/cli/shared/sandbox.js +417 -0
- package/src/cli/shared/scaffolding.js +535 -0
- package/src/cli/shared/toolchain-shims.js +420 -0
- package/src/context.js +229 -11
- package/src/core/runtime.js +6 -1
- package/src/doctor/index.js +42 -13
- package/src/finish/index.js +147 -5
- package/src/finish/preflight.js +177 -0
- package/src/finish/review-gate.js +182 -0
- package/src/git/index.js +446 -4
- package/src/hooks/index.js +0 -64
- package/src/mcp/collect.js +370 -0
- package/src/mcp/server.js +157 -0
- package/src/output/index.js +67 -1
- package/src/pr-review.js +23 -0
- package/src/pr.js +381 -0
- package/src/sandbox/index.js +13 -2
- package/src/scaffold/agent-worktree-prep.js +213 -0
- package/src/scaffold/index.js +108 -10
- package/src/speckit/index.js +226 -0
- package/src/terminal/index.js +1 -76
- package/src/terminal/tmux.js +0 -1
- package/src/toolchain/index.js +20 -0
- package/templates/AGENTS.monorepo-apps.md +26 -0
- package/templates/AGENTS.multiagent-safety.md +61 -347
- package/templates/AGENTS.multiagent-safety.min.md +11 -0
- package/templates/codex/skills/gx-act/SKILL.md +82 -0
- package/templates/githooks/pre-commit +22 -19
- package/templates/scripts/agent-branch-finish.sh +8 -30
- package/templates/scripts/agent-branch-merge.sh +4 -1
- package/templates/scripts/agent-branch-start.sh +88 -3
- package/templates/scripts/agent-preflight.sh +31 -5
- package/templates/scripts/agent-worktree-prune.sh +1 -1
- package/templates/scripts/codex-agent.sh +0 -91
- package/src/agents/detect.js +0 -160
- package/src/cockpit/keybindings.js +0 -224
- 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
|
|
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,
|
package/src/core/runtime.js
CHANGED
|
@@ -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
|
|
22
|
+
return cachedSpawn(cmd, args, {
|
|
18
23
|
encoding: 'utf8',
|
|
19
24
|
stdio: options.stdio || 'pipe',
|
|
20
25
|
cwd: options.cwd,
|
package/src/doctor/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|