@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
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
// Shared "internal" scaffolding helpers (install / fix / scan) and the
|
|
2
|
+
// pretty-printers used by multiple subcommands (status, doctor, setup, ...).
|
|
3
|
+
// Pure code-motion from src/cli/main.js — no behavior changes.
|
|
4
|
+
const {
|
|
5
|
+
fs,
|
|
6
|
+
path,
|
|
7
|
+
TOOL_NAME,
|
|
8
|
+
SHORT_TOOL_NAME,
|
|
9
|
+
HOOK_NAMES,
|
|
10
|
+
TEMPLATE_FILES,
|
|
11
|
+
LEGACY_WORKFLOW_SHIM_SPECS,
|
|
12
|
+
REQUIRED_MANAGED_REPO_FILES,
|
|
13
|
+
LOCK_FILE_RELATIVE,
|
|
14
|
+
OMX_SCAFFOLD_DIRECTORIES,
|
|
15
|
+
OMX_SCAFFOLD_FILES,
|
|
16
|
+
CRITICAL_GUARDRAIL_PATHS,
|
|
17
|
+
TARGETED_FORCEABLE_MANAGED_PATHS,
|
|
18
|
+
} = require('../../context');
|
|
19
|
+
const {
|
|
20
|
+
gitRun,
|
|
21
|
+
resolveRepoRoot,
|
|
22
|
+
gitRefExists,
|
|
23
|
+
readBranchDisplayName,
|
|
24
|
+
lockRegistryStatus,
|
|
25
|
+
} = require('../../git');
|
|
26
|
+
const {
|
|
27
|
+
toDestinationPath,
|
|
28
|
+
ensureGeneratedScriptShim,
|
|
29
|
+
ensureHookShim,
|
|
30
|
+
copyTemplateFile,
|
|
31
|
+
ensureTemplateFilePresent,
|
|
32
|
+
materializePackageRepoTemplateFiles,
|
|
33
|
+
ensureOmxScaffold,
|
|
34
|
+
ensureLockRegistry,
|
|
35
|
+
lockStateOrError,
|
|
36
|
+
writeLockState,
|
|
37
|
+
ensureAgentsSnippet,
|
|
38
|
+
ensureClaudeAgentsLink,
|
|
39
|
+
ensureMonorepoAppsSnippet,
|
|
40
|
+
ensureManagedGitignore,
|
|
41
|
+
ensureRepoVscodeSettings,
|
|
42
|
+
configureHooks,
|
|
43
|
+
} = require('../../scaffold');
|
|
44
|
+
const { colorizeDoctorOutput } = require('../../output');
|
|
45
|
+
const { normalizeManagedForcePath } = require('../args');
|
|
46
|
+
const {
|
|
47
|
+
resolveGuardexRepoToggle,
|
|
48
|
+
describeGuardexRepoToggle,
|
|
49
|
+
} = require('./repo-env');
|
|
50
|
+
|
|
51
|
+
function appendForceArgs(args, options) {
|
|
52
|
+
if (!options.force) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
args.push('--force');
|
|
56
|
+
for (const managedPath of options.forceManagedPaths || []) {
|
|
57
|
+
args.push(managedPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function shouldForceManagedPath(options, relativePath) {
|
|
62
|
+
if (!options.force) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
|
|
66
|
+
if (targetedPaths.length === 0) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const normalized = normalizeManagedForcePath(relativePath);
|
|
70
|
+
return normalized !== null && targetedPaths.includes(normalized);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureTargetedLegacyWorkflowShims(repoRoot, options) {
|
|
74
|
+
const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
|
|
75
|
+
if (targetedPaths.length === 0) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const operations = [];
|
|
80
|
+
for (const shim of LEGACY_WORKFLOW_SHIM_SPECS) {
|
|
81
|
+
if (!shouldForceManagedPath(options, shim.relativePath)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
operations.push(ensureGeneratedScriptShim(repoRoot, shim, { dryRun: options.dryRun, force: true }));
|
|
85
|
+
}
|
|
86
|
+
return operations;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findStaleLockPaths(repoRoot, locks) {
|
|
90
|
+
const stale = [];
|
|
91
|
+
|
|
92
|
+
for (const [filePath, rawEntry] of Object.entries(locks)) {
|
|
93
|
+
const entry = rawEntry && typeof rawEntry === 'object' ? rawEntry : {};
|
|
94
|
+
const ownerBranch = String(entry.branch || '');
|
|
95
|
+
|
|
96
|
+
const hasOwner = ownerBranch.length > 0;
|
|
97
|
+
const localRef = hasOwner ? `refs/heads/${ownerBranch}` : null;
|
|
98
|
+
const remoteRef = hasOwner ? `refs/remotes/origin/${ownerBranch}` : null;
|
|
99
|
+
const branchExists = hasOwner
|
|
100
|
+
? gitRefExists(repoRoot, localRef) || gitRefExists(repoRoot, remoteRef)
|
|
101
|
+
: false;
|
|
102
|
+
|
|
103
|
+
const pathExists = fs.existsSync(path.join(repoRoot, filePath));
|
|
104
|
+
|
|
105
|
+
if (!hasOwner || !branchExists || !pathExists) {
|
|
106
|
+
stale.push(filePath);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return stale;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runInstallInternal(options) {
|
|
114
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
115
|
+
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
116
|
+
if (!guardexToggle.enabled) {
|
|
117
|
+
return {
|
|
118
|
+
repoRoot,
|
|
119
|
+
operations: [
|
|
120
|
+
{
|
|
121
|
+
status: 'skipped',
|
|
122
|
+
file: '.env',
|
|
123
|
+
note: `Guardex disabled by ${describeGuardexRepoToggle(guardexToggle)}`,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
hookResult: { status: 'skipped', key: 'core.hooksPath', value: '(unchanged)' },
|
|
127
|
+
guardexEnabled: false,
|
|
128
|
+
guardexToggle,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const operations = [];
|
|
132
|
+
|
|
133
|
+
if (!options.skipGitignore) {
|
|
134
|
+
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
135
|
+
}
|
|
136
|
+
operations.push(ensureRepoVscodeSettings(repoRoot, Boolean(options.dryRun)));
|
|
137
|
+
|
|
138
|
+
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
139
|
+
|
|
140
|
+
for (const templateFile of TEMPLATE_FILES) {
|
|
141
|
+
operations.push(
|
|
142
|
+
copyTemplateFile(
|
|
143
|
+
repoRoot,
|
|
144
|
+
templateFile,
|
|
145
|
+
shouldForceManagedPath(options, toDestinationPath(templateFile)),
|
|
146
|
+
Boolean(options.dryRun),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
|
|
151
|
+
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
152
|
+
for (const hookName of HOOK_NAMES) {
|
|
153
|
+
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
154
|
+
operations.push(
|
|
155
|
+
ensureHookShim(repoRoot, hookName, {
|
|
156
|
+
dryRun: options.dryRun,
|
|
157
|
+
force: shouldForceManagedPath(options, hookRelativePath),
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
163
|
+
|
|
164
|
+
if (!options.skipAgents) {
|
|
165
|
+
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force), contract: Boolean(options.contract) }));
|
|
166
|
+
operations.push(ensureMonorepoAppsSnippet(repoRoot, Boolean(options.dryRun)));
|
|
167
|
+
operations.push(ensureClaudeAgentsLink(repoRoot, Boolean(options.dryRun)));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
|
|
171
|
+
|
|
172
|
+
return { repoRoot, operations, hookResult, guardexEnabled: true, guardexToggle };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function runFixInternal(options) {
|
|
176
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
177
|
+
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
178
|
+
if (!guardexToggle.enabled) {
|
|
179
|
+
return {
|
|
180
|
+
repoRoot,
|
|
181
|
+
operations: [
|
|
182
|
+
{
|
|
183
|
+
status: 'skipped',
|
|
184
|
+
file: '.env',
|
|
185
|
+
note: `Guardex disabled by ${describeGuardexRepoToggle(guardexToggle)}`,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
hookResult: { status: 'skipped', key: 'core.hooksPath', value: '(unchanged)' },
|
|
189
|
+
guardexEnabled: false,
|
|
190
|
+
guardexToggle,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const operations = [];
|
|
194
|
+
|
|
195
|
+
if (!options.skipGitignore) {
|
|
196
|
+
operations.push(ensureManagedGitignore(repoRoot, Boolean(options.dryRun)));
|
|
197
|
+
}
|
|
198
|
+
operations.push(ensureRepoVscodeSettings(repoRoot, Boolean(options.dryRun)));
|
|
199
|
+
|
|
200
|
+
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
201
|
+
|
|
202
|
+
for (const templateFile of TEMPLATE_FILES) {
|
|
203
|
+
if (shouldForceManagedPath(options, toDestinationPath(templateFile))) {
|
|
204
|
+
operations.push(copyTemplateFile(repoRoot, templateFile, true, Boolean(options.dryRun)));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
|
|
208
|
+
}
|
|
209
|
+
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
|
|
210
|
+
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
211
|
+
for (const hookName of HOOK_NAMES) {
|
|
212
|
+
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
213
|
+
operations.push(
|
|
214
|
+
ensureHookShim(repoRoot, hookName, {
|
|
215
|
+
dryRun: options.dryRun,
|
|
216
|
+
force: shouldForceManagedPath(options, hookRelativePath),
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
222
|
+
|
|
223
|
+
const lockState = lockStateOrError(repoRoot);
|
|
224
|
+
if (!lockState.ok) {
|
|
225
|
+
if (!options.dryRun) {
|
|
226
|
+
writeLockState(repoRoot, { locks: {} }, false);
|
|
227
|
+
}
|
|
228
|
+
operations.push({
|
|
229
|
+
status: options.dryRun ? 'would-reset' : 'reset',
|
|
230
|
+
file: LOCK_FILE_RELATIVE,
|
|
231
|
+
note: 'invalid lock state reset to empty',
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
const staleLockPaths = options.dropStaleLocks ? findStaleLockPaths(repoRoot, lockState.locks) : [];
|
|
235
|
+
if (staleLockPaths.length > 0) {
|
|
236
|
+
const updated = { ...lockState.raw, locks: { ...lockState.locks } };
|
|
237
|
+
for (const filePath of staleLockPaths) {
|
|
238
|
+
delete updated.locks[filePath];
|
|
239
|
+
}
|
|
240
|
+
writeLockState(repoRoot, updated, Boolean(options.dryRun));
|
|
241
|
+
operations.push({
|
|
242
|
+
status: options.dryRun ? 'would-prune' : 'pruned',
|
|
243
|
+
file: LOCK_FILE_RELATIVE,
|
|
244
|
+
note: `removed ${staleLockPaths.length} stale lock(s)`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!options.skipAgents) {
|
|
250
|
+
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force), contract: Boolean(options.contract) }));
|
|
251
|
+
operations.push(ensureMonorepoAppsSnippet(repoRoot, Boolean(options.dryRun)));
|
|
252
|
+
operations.push(ensureClaudeAgentsLink(repoRoot, Boolean(options.dryRun)));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
|
|
256
|
+
|
|
257
|
+
return { repoRoot, operations, hookResult, guardexEnabled: true, guardexToggle };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function runScanInternal(options) {
|
|
261
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
262
|
+
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
263
|
+
const branch = readBranchDisplayName(repoRoot);
|
|
264
|
+
if (!guardexToggle.enabled) {
|
|
265
|
+
return {
|
|
266
|
+
repoRoot,
|
|
267
|
+
branch,
|
|
268
|
+
findings: [],
|
|
269
|
+
errors: 0,
|
|
270
|
+
warnings: 0,
|
|
271
|
+
guardexEnabled: false,
|
|
272
|
+
guardexToggle,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const findings = [];
|
|
276
|
+
|
|
277
|
+
const requiredPaths = [
|
|
278
|
+
...OMX_SCAFFOLD_DIRECTORIES,
|
|
279
|
+
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
280
|
+
...REQUIRED_MANAGED_REPO_FILES,
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
for (const relativePath of requiredPaths) {
|
|
284
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
285
|
+
if (!fs.existsSync(absolutePath)) {
|
|
286
|
+
findings.push({
|
|
287
|
+
level: 'error',
|
|
288
|
+
code: 'missing-managed-file',
|
|
289
|
+
path: relativePath,
|
|
290
|
+
message: `Missing managed repo file: ${relativePath}`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const hooksPathResult = gitRun(repoRoot, ['config', '--get', 'core.hooksPath'], { allowFailure: true });
|
|
296
|
+
const hooksPath = hooksPathResult.status === 0 ? hooksPathResult.stdout.trim() : '';
|
|
297
|
+
if (hooksPath !== '.githooks') {
|
|
298
|
+
findings.push({
|
|
299
|
+
level: 'warn',
|
|
300
|
+
code: 'hooks-path-mismatch',
|
|
301
|
+
message: `git core.hooksPath is '${hooksPath || '(unset)'}' (expected '.githooks')`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const lockState = lockStateOrError(repoRoot);
|
|
306
|
+
if (!lockState.ok) {
|
|
307
|
+
findings.push({
|
|
308
|
+
level: 'error',
|
|
309
|
+
code: 'lock-state-invalid',
|
|
310
|
+
message: lockState.error,
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
for (const [filePath, rawEntry] of Object.entries(lockState.locks)) {
|
|
314
|
+
const entry = rawEntry && typeof rawEntry === 'object' ? rawEntry : {};
|
|
315
|
+
const ownerBranch = String(entry.branch || '');
|
|
316
|
+
const allowDelete = Boolean(entry.allow_delete);
|
|
317
|
+
|
|
318
|
+
if (!ownerBranch) {
|
|
319
|
+
findings.push({
|
|
320
|
+
level: 'warn',
|
|
321
|
+
code: 'lock-missing-owner',
|
|
322
|
+
path: filePath,
|
|
323
|
+
message: `Lock entry has no owner branch: ${filePath}`,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const absolutePath = path.join(repoRoot, filePath);
|
|
328
|
+
if (!fs.existsSync(absolutePath)) {
|
|
329
|
+
findings.push({
|
|
330
|
+
level: 'warn',
|
|
331
|
+
code: 'lock-target-missing',
|
|
332
|
+
path: filePath,
|
|
333
|
+
message: `Locked path is missing from disk: ${filePath}`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (ownerBranch) {
|
|
338
|
+
const localRef = `refs/heads/${ownerBranch}`;
|
|
339
|
+
const remoteRef = `refs/remotes/origin/${ownerBranch}`;
|
|
340
|
+
if (!gitRefExists(repoRoot, localRef) && !gitRefExists(repoRoot, remoteRef)) {
|
|
341
|
+
findings.push({
|
|
342
|
+
level: 'warn',
|
|
343
|
+
code: 'stale-branch-lock',
|
|
344
|
+
path: filePath,
|
|
345
|
+
message: `Lock owner branch not found locally/remotely: ${ownerBranch} (${filePath})`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (allowDelete && CRITICAL_GUARDRAIL_PATHS.has(filePath)) {
|
|
351
|
+
findings.push({
|
|
352
|
+
level: 'error',
|
|
353
|
+
code: 'guardrail-delete-approved',
|
|
354
|
+
path: filePath,
|
|
355
|
+
message: `Critical guardrail file is delete-approved: ${filePath}`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const errors = findings.filter((item) => item.level === 'error');
|
|
362
|
+
const warnings = findings.filter((item) => item.level === 'warn');
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
repoRoot,
|
|
366
|
+
branch,
|
|
367
|
+
findings,
|
|
368
|
+
errors: errors.length,
|
|
369
|
+
warnings: warnings.length,
|
|
370
|
+
guardexEnabled: true,
|
|
371
|
+
guardexToggle,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function printWorktreePruneSummary(payload, options = {}) {
|
|
376
|
+
if (!payload || payload.enabled === false) {
|
|
377
|
+
if (payload && payload.details && payload.details[0]) {
|
|
378
|
+
console.log(`[${TOOL_NAME}] ${payload.details[0]}`);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (!payload.ran) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const baseLabel = options.baseBranch ? ` (base=${options.baseBranch})` : '';
|
|
386
|
+
const tag = payload.status === 'failed' ? '⚠️' : (payload.status === 'dry-run' ? '🔍' : '🧹');
|
|
387
|
+
console.log(
|
|
388
|
+
`[${TOOL_NAME}] ${tag} Stale agent-worktree prune${baseLabel}: status=${payload.status}`,
|
|
389
|
+
);
|
|
390
|
+
for (const detail of payload.details || []) {
|
|
391
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function printScanResult(scan, json = false) {
|
|
396
|
+
if (json) {
|
|
397
|
+
process.stdout.write(
|
|
398
|
+
JSON.stringify(
|
|
399
|
+
{
|
|
400
|
+
repoRoot: scan.repoRoot,
|
|
401
|
+
branch: scan.branch,
|
|
402
|
+
guardexEnabled: scan.guardexEnabled !== false,
|
|
403
|
+
guardexToggle: scan.guardexToggle || null,
|
|
404
|
+
errors: scan.errors,
|
|
405
|
+
warnings: scan.warnings,
|
|
406
|
+
findings: scan.findings,
|
|
407
|
+
},
|
|
408
|
+
null,
|
|
409
|
+
2,
|
|
410
|
+
) + '\n',
|
|
411
|
+
);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
console.log(`[${TOOL_NAME}] Scan target: ${scan.repoRoot}`);
|
|
416
|
+
console.log(`[${TOOL_NAME}] Branch: ${scan.branch}`);
|
|
417
|
+
|
|
418
|
+
if (scan.guardexEnabled === false) {
|
|
419
|
+
console.log(
|
|
420
|
+
colorizeDoctorOutput(
|
|
421
|
+
`[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(scan.guardexToggle)}).`,
|
|
422
|
+
'disabled',
|
|
423
|
+
),
|
|
424
|
+
);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (scan.findings.length === 0) {
|
|
429
|
+
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ✅ No safety issues detected.`, 'safe'));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
for (const item of scan.findings) {
|
|
434
|
+
const target = item.path ? ` (${item.path})` : '';
|
|
435
|
+
console.log(
|
|
436
|
+
colorizeDoctorOutput(
|
|
437
|
+
`[${item.level.toUpperCase()}] ${item.code}${target}: ${item.message}`,
|
|
438
|
+
item.level,
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
console.log(
|
|
443
|
+
colorizeDoctorOutput(
|
|
444
|
+
`[${TOOL_NAME}] Summary: ${scan.errors} error(s), ${scan.warnings} warning(s).`,
|
|
445
|
+
scan.errors > 0 ? 'error' : 'warn',
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function setExitCodeFromScan(scan) {
|
|
451
|
+
if (scan.guardexEnabled === false) {
|
|
452
|
+
process.exitCode = 0;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (scan.errors > 0) {
|
|
456
|
+
process.exitCode = 2;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (scan.warnings > 0) {
|
|
460
|
+
process.exitCode = 1;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
process.exitCode = 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function printStatusRepairHint(scanResult) {
|
|
467
|
+
if (!scanResult || scanResult.guardexEnabled === false) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const scanHint = scanResult.errors === 0
|
|
475
|
+
? `review warning details with '${SHORT_TOOL_NAME} scan'`
|
|
476
|
+
: `inspect detailed findings with '${SHORT_TOOL_NAME} scan'`;
|
|
477
|
+
console.log(
|
|
478
|
+
`[${TOOL_NAME}] Quick fix: run '${SHORT_TOOL_NAME} doctor' to repair drift, or ${scanHint}.`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function countAgentWorktrees(repoRoot) {
|
|
483
|
+
if (!repoRoot || typeof repoRoot !== 'string') return 0;
|
|
484
|
+
const relPaths = ['.omc/agent-worktrees', '.omx/agent-worktrees'];
|
|
485
|
+
let count = 0;
|
|
486
|
+
for (const rel of relPaths) {
|
|
487
|
+
try {
|
|
488
|
+
const entries = fs.readdirSync(path.join(repoRoot, rel), { withFileTypes: true });
|
|
489
|
+
count += entries.filter((entry) => entry.isDirectory()).length;
|
|
490
|
+
} catch (_err) {
|
|
491
|
+
// missing dir or permission error; not an active-agent signal
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return count;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function deriveNextStepHint({ scanResult, worktreeCount, invoked, inGitRepo }) {
|
|
498
|
+
if (!inGitRepo) {
|
|
499
|
+
return `${invoked} setup --target <path-to-git-repo> # initialize guardrails in a repo`;
|
|
500
|
+
}
|
|
501
|
+
if (!scanResult) {
|
|
502
|
+
return `${invoked} setup # bootstrap repo guardrails`;
|
|
503
|
+
}
|
|
504
|
+
if (scanResult.guardexEnabled === false) {
|
|
505
|
+
return `set GUARDEX_ON=1 in .env # re-enable guardrails, then '${invoked} doctor'`;
|
|
506
|
+
}
|
|
507
|
+
const branch = scanResult.branch || '';
|
|
508
|
+
if (branch.startsWith('agent/')) {
|
|
509
|
+
return `${invoked} branch finish --branch "${branch}" --via-pr --wait-for-merge --cleanup`;
|
|
510
|
+
}
|
|
511
|
+
if (worktreeCount > 0) {
|
|
512
|
+
const plural = worktreeCount === 1 ? 'worktree' : 'worktrees';
|
|
513
|
+
return `${invoked} finish --all # ${worktreeCount} active agent ${plural}`;
|
|
514
|
+
}
|
|
515
|
+
if (scanResult.errors > 0 || scanResult.warnings > 0) {
|
|
516
|
+
return `${invoked} doctor # repair drift`;
|
|
517
|
+
}
|
|
518
|
+
return `${invoked} branch start "<task>" "<agent-name>" # start a sandboxed agent task`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
module.exports = {
|
|
522
|
+
appendForceArgs,
|
|
523
|
+
shouldForceManagedPath,
|
|
524
|
+
ensureTargetedLegacyWorkflowShims,
|
|
525
|
+
findStaleLockPaths,
|
|
526
|
+
runInstallInternal,
|
|
527
|
+
runFixInternal,
|
|
528
|
+
runScanInternal,
|
|
529
|
+
printWorktreePruneSummary,
|
|
530
|
+
printScanResult,
|
|
531
|
+
setExitCodeFromScan,
|
|
532
|
+
printStatusRepairHint,
|
|
533
|
+
countAgentWorktrees,
|
|
534
|
+
deriveNextStepHint,
|
|
535
|
+
};
|