@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/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 || path.resolve(PACKAGE_ROOT),
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 results = [];
57
- const seen = new Set();
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
- if (depth >= maxDepth) {
67
- return;
68
- }
70
+ function shouldSkipDir(dirName) {
71
+ return NESTED_REPO_DEFAULT_SKIP_DIRS.has(dirName) || extraSkip.has(dirName);
72
+ }
69
73
 
70
- let entries = [];
74
+ function walk(currentPath, depth) {
75
+ if (depth > maxDepth) return;
76
+ let entries;
71
77
  try {
72
- entries = require('node:fs').readdirSync(directoryPath, { withFileTypes: true });
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
- if (!entry.isDirectory()) {
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
- const childPath = path.join(directoryPath, entry.name);
86
- const gitDir = path.join(childPath, '.git');
87
- if (require('node:fs').existsSync(gitDir)) {
88
- if (!includeSubmodules) {
89
- const gitInfo = require('node:fs').lstatSync(gitDir);
90
- if (gitInfo.isFile()) {
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
- visit(childPath, depth + 1);
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
- visit(resolvedRoot, 0);
103
- return results;
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 = {
@@ -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
- OPENSPEC_TIER="T1"
132
- TASK_ROUTING_REASON="explicit lightweight prefix"
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
- session.pidAlive === false ? `PID ${session.pid} not alive` : `PID ${session.pid} alive`,
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
- `Started ${session.startedAt}`,
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 vscode.workspace.findFiles(
483
- ACTIVE_SESSION_FILES_GLOB,
484
- SESSION_SCAN_EXCLUDE_GLOB,
485
- SESSION_SCAN_LIMIT,
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
  }
@@ -14,6 +14,8 @@
14
14
  ],
15
15
  "activationEvents": [
16
16
  "workspaceContains:.omx/state/active-sessions",
17
+ "workspaceContains:.omx/agent-worktrees",
18
+ "workspaceContains:.omc/agent-worktrees",
17
19
  "onView:gitguardex.activeAgents"
18
20
  ],
19
21
  "main": "./extension.js",