@imdeadpool/guardex 7.0.20 → 7.0.21
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 +44 -12
- package/package.json +1 -1
- package/src/cli/args.js +804 -2
- package/src/cli/main.js +173 -2328
- package/src/context.js +17 -3
- package/src/git/index.js +42 -32
- package/src/scaffold/index.js +1 -22
- package/templates/scripts/codex-agent.sh +14 -2
- package/templates/vscode/guardex-active-agents/README.md +1 -1
- package/templates/vscode/guardex-active-agents/extension.js +38 -7
- package/templates/vscode/guardex-active-agents/package.json +2 -0
- package/templates/vscode/guardex-active-agents/session-schema.js +233 -29
package/src/context.js
CHANGED
|
@@ -71,7 +71,7 @@ const REQUIRED_SYSTEM_TOOLS = [
|
|
|
71
71
|
},
|
|
72
72
|
];
|
|
73
73
|
const MAINTAINER_RELEASE_REPO = path.resolve(
|
|
74
|
-
process.env.GUARDEX_RELEASE_REPO ||
|
|
74
|
+
process.env.GUARDEX_RELEASE_REPO || PACKAGE_ROOT,
|
|
75
75
|
);
|
|
76
76
|
const NPM_BIN = process.env.GUARDEX_NPM_BIN || 'npm';
|
|
77
77
|
const OPENSPEC_BIN = process.env.GUARDEX_OPENSPEC_BIN || 'openspec';
|
|
@@ -242,13 +242,24 @@ const GITIGNORE_MARKER_START = '# multiagent-safety:START';
|
|
|
242
242
|
const GITIGNORE_MARKER_END = '# multiagent-safety:END';
|
|
243
243
|
const CODEX_WORKTREE_RELATIVE_DIR = path.join('.omx', 'agent-worktrees');
|
|
244
244
|
const CLAUDE_WORKTREE_RELATIVE_DIR = path.join('.omc', 'agent-worktrees');
|
|
245
|
+
const SHARED_VSCODE_SETTINGS_RELATIVE = path.posix.join('.vscode', 'settings.json');
|
|
246
|
+
const REPO_SCAN_IGNORED_FOLDERS_SETTING = 'git.repositoryScanIgnoredFolders';
|
|
245
247
|
const AGENT_WORKTREE_RELATIVE_DIRS = [
|
|
246
248
|
CODEX_WORKTREE_RELATIVE_DIR,
|
|
247
249
|
CLAUDE_WORKTREE_RELATIVE_DIR,
|
|
248
250
|
];
|
|
251
|
+
const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [
|
|
252
|
+
'.omx/agent-worktrees',
|
|
253
|
+
'**/.omx/agent-worktrees',
|
|
254
|
+
'.omc/agent-worktrees',
|
|
255
|
+
'**/.omc/agent-worktrees',
|
|
256
|
+
];
|
|
249
257
|
const MANAGED_GITIGNORE_PATHS = [
|
|
250
258
|
'.omx/',
|
|
251
259
|
'.omc/',
|
|
260
|
+
'!.vscode/',
|
|
261
|
+
'.vscode/*',
|
|
262
|
+
'!.vscode/settings.json',
|
|
252
263
|
'scripts/agent-session-state.js',
|
|
253
264
|
'scripts/guardex-docker-loader.sh',
|
|
254
265
|
'scripts/guardex-env.sh',
|
|
@@ -322,8 +333,8 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
322
333
|
];
|
|
323
334
|
const CLI_COMMAND_DESCRIPTIONS = [
|
|
324
335
|
['status', 'Show GitGuardex CLI + service health without modifying files'],
|
|
325
|
-
['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
|
|
326
|
-
['doctor', 'Repair drift + verify (auto-sandboxes on protected main)'],
|
|
336
|
+
['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target, --current)'],
|
|
337
|
+
['doctor', 'Repair drift + verify (flags: --target, --current; auto-sandboxes on protected main)'],
|
|
327
338
|
['branch', 'CLI-owned branch workflow surface (start/finish/merge)'],
|
|
328
339
|
['locks', 'CLI-owned file lock surface (claim/allow-delete/release/status/validate)'],
|
|
329
340
|
['worktree', 'CLI-owned worktree cleanup surface (prune)'],
|
|
@@ -480,7 +491,10 @@ module.exports = {
|
|
|
480
491
|
GITIGNORE_MARKER_END,
|
|
481
492
|
CODEX_WORKTREE_RELATIVE_DIR,
|
|
482
493
|
CLAUDE_WORKTREE_RELATIVE_DIR,
|
|
494
|
+
SHARED_VSCODE_SETTINGS_RELATIVE,
|
|
495
|
+
REPO_SCAN_IGNORED_FOLDERS_SETTING,
|
|
483
496
|
AGENT_WORKTREE_RELATIVE_DIRS,
|
|
497
|
+
MANAGED_REPO_SCAN_IGNORED_FOLDERS,
|
|
484
498
|
MANAGED_GITIGNORE_PATHS,
|
|
485
499
|
REPO_SCAFFOLD_DIRECTORIES,
|
|
486
500
|
OMX_SCAFFOLD_DIRECTORIES,
|
package/src/git/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
1
2
|
const { path } = require('../context');
|
|
2
3
|
const { run } = require('../core/runtime');
|
|
3
4
|
|
|
@@ -41,66 +42,75 @@ const NESTED_REPO_DEFAULT_SKIP_DIRS = new Set([
|
|
|
41
42
|
'.pnpm-store',
|
|
42
43
|
]);
|
|
43
44
|
|
|
45
|
+
function resolveGitCommonDir(repoPath) {
|
|
46
|
+
const result = run('git', ['-C', repoPath, 'rev-parse', '--git-common-dir'], { cwd: repoPath });
|
|
47
|
+
if (result.status !== 0) return null;
|
|
48
|
+
const raw = result.stdout.trim();
|
|
49
|
+
if (!raw) return null;
|
|
50
|
+
return path.resolve(repoPath, raw);
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
function discoverNestedGitRepos(rootPath, opts = {}) {
|
|
45
54
|
const maxDepth = Number.isFinite(opts.maxDepth)
|
|
46
55
|
? Math.max(1, opts.maxDepth)
|
|
47
56
|
: NESTED_REPO_DEFAULT_MAX_DEPTH;
|
|
48
57
|
const extraSkip = new Set(Array.isArray(opts.extraSkip) ? opts.extraSkip : []);
|
|
49
58
|
const includeSubmodules = Boolean(opts.includeSubmodules);
|
|
59
|
+
const skipRelativeDirs = Array.isArray(opts.skipRelativeDirs) ? opts.skipRelativeDirs.filter(Boolean) : [];
|
|
50
60
|
const resolvedRoot = path.resolve(rootPath);
|
|
51
61
|
|
|
52
62
|
if (!isGitRepo(resolvedRoot)) {
|
|
53
63
|
throw new Error(`Target is not inside a git repository: ${resolvedRoot}`);
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
function visit(directoryPath, depth) {
|
|
60
|
-
const repoRoot = resolveRepoRoot(directoryPath);
|
|
61
|
-
if (!seen.has(repoRoot)) {
|
|
62
|
-
seen.add(repoRoot);
|
|
63
|
-
results.push(repoRoot);
|
|
64
|
-
}
|
|
66
|
+
const rootCommonDir = resolveGitCommonDir(resolvedRoot);
|
|
67
|
+
const skipAbsolutes = skipRelativeDirs.map((relativeDir) => path.join(resolvedRoot, relativeDir));
|
|
68
|
+
const found = new Set([resolvedRoot]);
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
function shouldSkipDir(dirName) {
|
|
71
|
+
return NESTED_REPO_DEFAULT_SKIP_DIRS.has(dirName) || extraSkip.has(dirName);
|
|
72
|
+
}
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
function walk(currentPath, depth) {
|
|
75
|
+
if (depth > maxDepth) return;
|
|
76
|
+
let entries;
|
|
71
77
|
try {
|
|
72
|
-
entries =
|
|
78
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
73
79
|
} catch {
|
|
74
80
|
return;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
for (const entry of entries) {
|
|
78
|
-
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (NESTED_REPO_DEFAULT_SKIP_DIRS.has(entry.name) || extraSkip.has(entry.name)) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
86
|
+
if (entry.name === '.git') {
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
if (entryPath === path.join(resolvedRoot, '.git')) continue;
|
|
89
|
+
found.add(path.dirname(entryPath));
|
|
90
|
+
} else if (includeSubmodules && entry.isFile()) {
|
|
91
|
+
found.add(path.dirname(entryPath));
|
|
93
92
|
}
|
|
94
|
-
visit(childPath, depth + 1);
|
|
95
93
|
continue;
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
97
|
+
if (shouldSkipDir(entry.name)) continue;
|
|
98
|
+
if (skipAbsolutes.includes(entryPath)) continue;
|
|
99
|
+
walk(entryPath, depth + 1);
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
walk(resolvedRoot, 0);
|
|
104
|
+
|
|
105
|
+
const filtered = Array.from(found).filter((repoPath) => {
|
|
106
|
+
if (repoPath === resolvedRoot || !rootCommonDir) return true;
|
|
107
|
+
const childCommonDir = resolveGitCommonDir(repoPath);
|
|
108
|
+
return !childCommonDir || childCommonDir !== rootCommonDir;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const [root, ...rest] = filtered;
|
|
112
|
+
rest.sort((a, b) => a.localeCompare(b));
|
|
113
|
+
return root ? [root, ...rest] : [];
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
module.exports = {
|
package/src/scaffold/index.js
CHANGED
|
@@ -3,32 +3,11 @@ const {
|
|
|
3
3
|
path,
|
|
4
4
|
TOOL_NAME,
|
|
5
5
|
SHORT_TOOL_NAME,
|
|
6
|
+
toDestinationPath,
|
|
6
7
|
EXECUTABLE_RELATIVE_PATHS,
|
|
7
8
|
CRITICAL_GUARDRAIL_PATHS,
|
|
8
9
|
} = require('../context');
|
|
9
10
|
|
|
10
|
-
function toDestinationPath(relativeTemplatePath) {
|
|
11
|
-
if (relativeTemplatePath.startsWith('scripts/')) {
|
|
12
|
-
return relativeTemplatePath;
|
|
13
|
-
}
|
|
14
|
-
if (relativeTemplatePath.startsWith('githooks/')) {
|
|
15
|
-
return `.${relativeTemplatePath}`;
|
|
16
|
-
}
|
|
17
|
-
if (relativeTemplatePath.startsWith('codex/')) {
|
|
18
|
-
return `.${relativeTemplatePath}`;
|
|
19
|
-
}
|
|
20
|
-
if (relativeTemplatePath.startsWith('claude/')) {
|
|
21
|
-
return `.${relativeTemplatePath}`;
|
|
22
|
-
}
|
|
23
|
-
if (relativeTemplatePath.startsWith('github/')) {
|
|
24
|
-
return `.${relativeTemplatePath}`;
|
|
25
|
-
}
|
|
26
|
-
if (relativeTemplatePath.startsWith('vscode/')) {
|
|
27
|
-
return relativeTemplatePath;
|
|
28
|
-
}
|
|
29
|
-
throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
11
|
function ensureParentDir(repoRoot, filePath, dryRun) {
|
|
33
12
|
if (dryRun) return;
|
|
34
13
|
|
|
@@ -90,6 +90,13 @@ string_has_lightweight_prefix() {
|
|
|
90
90
|
return 1
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
task_requires_full_change_workspace() {
|
|
94
|
+
local text="$1"
|
|
95
|
+
string_contains_any "$text" \
|
|
96
|
+
"cleanup evidence" "merged cleanup" "merged state" "pr url" \
|
|
97
|
+
"cleanup pipeline" "finish pipeline" "sandbox cleanup" "tasks.md"
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
derive_task_mode_from_tier() {
|
|
94
101
|
case "$1" in
|
|
95
102
|
T0|T1) printf 'caveman' ;;
|
|
@@ -128,8 +135,13 @@ decide_task_routing() {
|
|
|
128
135
|
fi
|
|
129
136
|
TASK_ROUTING_REASON="explicit tier override"
|
|
130
137
|
elif string_has_lightweight_prefix "$task_lower"; then
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
if task_requires_full_change_workspace "$task_lower"; then
|
|
139
|
+
OPENSPEC_TIER="T2"
|
|
140
|
+
TASK_ROUTING_REASON="cleanup-evidence artifact wording overrides lightweight prefix"
|
|
141
|
+
else
|
|
142
|
+
OPENSPEC_TIER="T1"
|
|
143
|
+
TASK_ROUTING_REASON="explicit lightweight prefix"
|
|
144
|
+
fi
|
|
133
145
|
elif string_contains_any "$task_lower" \
|
|
134
146
|
"ralph" "autopilot" "ultrawork" "ultraqa" "ralplan" "deep interview" "ouroboros" \
|
|
135
147
|
"migration" "refactor" "architecture" "re-architect" "cross-cutting" "multi-agent" \
|
|
@@ -24,4 +24,4 @@ What it does:
|
|
|
24
24
|
- Shows repo-root git changes in a sibling `CHANGES` section when the guarded repo itself is dirty.
|
|
25
25
|
- Derives session state from dirty worktree status, git conflict markers, PID liveness, and recent file mtimes, surfaces working/dead counts in the repo/header summary, and shows changed-file counts for active edits.
|
|
26
26
|
- Uses distinct VS Code codicons for each session state: `warning`, `edit`, `loading~spin`, `clock`, and `error`.
|
|
27
|
-
- Reads repo-local presence files from `.omx/state/active-sessions
|
|
27
|
+
- Reads repo-local presence files from `.omx/state/active-sessions/` and falls back to managed worktree-root `AGENT.lock` telemetry when the launcher session file is absent.
|
|
@@ -15,7 +15,9 @@ const IDLE_ERROR_MS = 30 * 60 * 1000;
|
|
|
15
15
|
const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
|
|
16
16
|
const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json';
|
|
17
17
|
const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json';
|
|
18
|
+
const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock';
|
|
18
19
|
const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**';
|
|
20
|
+
const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**';
|
|
19
21
|
const SESSION_SCAN_LIMIT = 200;
|
|
20
22
|
const REFRESH_DEBOUNCE_MS = 250;
|
|
21
23
|
const SESSION_ACTIVITY_GROUPS = [
|
|
@@ -187,14 +189,23 @@ class SessionItem extends vscode.TreeItem {
|
|
|
187
189
|
const tooltipLines = [
|
|
188
190
|
session.branch,
|
|
189
191
|
`${session.agentName} · ${session.taskName}`,
|
|
192
|
+
session.latestTaskPreview && session.latestTaskPreview !== session.taskName
|
|
193
|
+
? `Live task ${session.latestTaskPreview}`
|
|
194
|
+
: '',
|
|
190
195
|
`Status ${this.description}`,
|
|
191
196
|
session.changeCount > 0
|
|
192
197
|
? `Changed ${session.activityCountLabel}: ${session.activitySummary}`
|
|
193
198
|
: session.activitySummary,
|
|
194
199
|
`Locks ${lockCount}`,
|
|
195
|
-
|
|
200
|
+
Number.isInteger(session.pid) && session.pid > 0
|
|
201
|
+
? session.pidAlive === false
|
|
202
|
+
? `PID ${session.pid} not alive`
|
|
203
|
+
: `PID ${session.pid} alive`
|
|
204
|
+
: '',
|
|
196
205
|
session.lastFileActivityAt ? `Last file activity ${session.lastFileActivityAt}` : '',
|
|
197
|
-
|
|
206
|
+
session.sourceKind === 'worktree-lock'
|
|
207
|
+
? `Telemetry updated ${session.telemetryUpdatedAt || session.startedAt}`
|
|
208
|
+
: `Started ${session.startedAt}`,
|
|
198
209
|
session.worktreePath,
|
|
199
210
|
];
|
|
200
211
|
this.tooltip = tooltipLines.filter(Boolean).join('\n');
|
|
@@ -365,6 +376,10 @@ function repoRootFromSessionFile(filePath) {
|
|
|
365
376
|
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
366
377
|
}
|
|
367
378
|
|
|
379
|
+
function repoRootFromWorktreeLockFile(filePath) {
|
|
380
|
+
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
381
|
+
}
|
|
382
|
+
|
|
368
383
|
function repoRootFromLockFile(filePath) {
|
|
369
384
|
return path.resolve(path.dirname(filePath), '..', '..');
|
|
370
385
|
}
|
|
@@ -479,16 +494,29 @@ function localizeChangeForSession(session, change) {
|
|
|
479
494
|
}
|
|
480
495
|
|
|
481
496
|
async function findRepoSessionEntries() {
|
|
482
|
-
const sessionFiles = await
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
497
|
+
const [sessionFiles, worktreeLockFiles] = await Promise.all([
|
|
498
|
+
vscode.workspace.findFiles(
|
|
499
|
+
ACTIVE_SESSION_FILES_GLOB,
|
|
500
|
+
SESSION_SCAN_EXCLUDE_GLOB,
|
|
501
|
+
SESSION_SCAN_LIMIT,
|
|
502
|
+
),
|
|
503
|
+
vscode.workspace.findFiles(
|
|
504
|
+
WORKTREE_AGENT_LOCKS_GLOB,
|
|
505
|
+
WORKTREE_LOCK_SCAN_EXCLUDE_GLOB,
|
|
506
|
+
SESSION_SCAN_LIMIT,
|
|
507
|
+
),
|
|
508
|
+
]);
|
|
487
509
|
|
|
488
510
|
const repoRoots = new Set();
|
|
489
511
|
for (const uri of sessionFiles) {
|
|
490
512
|
repoRoots.add(repoRootFromSessionFile(uri.fsPath));
|
|
491
513
|
}
|
|
514
|
+
for (const uri of worktreeLockFiles) {
|
|
515
|
+
if (path.basename(uri.fsPath) !== 'AGENT.lock') {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
repoRoots.add(repoRootFromWorktreeLockFile(uri.fsPath));
|
|
519
|
+
}
|
|
492
520
|
|
|
493
521
|
if (repoRoots.size === 0) {
|
|
494
522
|
for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
|
|
@@ -1057,6 +1085,7 @@ function activate(context) {
|
|
|
1057
1085
|
const refresh = () => void refreshController.refreshNow();
|
|
1058
1086
|
const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
|
|
1059
1087
|
const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
|
|
1088
|
+
const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB);
|
|
1060
1089
|
const updateCommitInput = (session) => {
|
|
1061
1090
|
sourceControl.inputBox.enabled = true;
|
|
1062
1091
|
sourceControl.inputBox.visible = true;
|
|
@@ -1151,12 +1180,14 @@ function activate(context) {
|
|
|
1151
1180
|
vscode.workspace.onDidChangeWorkspaceFolders(scheduleRefresh),
|
|
1152
1181
|
activeSessionsWatcher,
|
|
1153
1182
|
lockWatcher,
|
|
1183
|
+
worktreeLockWatcher,
|
|
1154
1184
|
{ dispose: () => clearInterval(interval) },
|
|
1155
1185
|
);
|
|
1156
1186
|
|
|
1157
1187
|
context.subscriptions.push(
|
|
1158
1188
|
...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh),
|
|
1159
1189
|
...bindRefreshWatcher(lockWatcher, refreshLockRegistry),
|
|
1190
|
+
...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
|
|
1160
1191
|
);
|
|
1161
1192
|
void refreshController.refreshNow();
|
|
1162
1193
|
}
|