@imdeadpool/guardex 7.0.22 → 7.0.23
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 +6 -0
- package/package.json +5 -1
- package/src/cli/args.js +89 -0
- package/src/cli/main.js +49 -75
- package/src/context.js +16 -2
- package/src/core/stdin.js +52 -0
- package/src/core/versions.js +33 -0
- package/src/hooks/index.js +64 -0
- package/src/output/index.js +64 -4
- package/src/report/session-severity.js +213 -0
- package/src/scaffold/index.js +78 -131
- package/src/toolchain/index.js +6 -70
- package/templates/AGENTS.multiagent-safety.md +25 -0
- package/templates/scripts/agent-branch-finish.sh +30 -1
- package/templates/scripts/agent-session-state.js +62 -1
- package/templates/scripts/codex-agent.sh +38 -0
- package/templates/scripts/install-vscode-active-agents-extension.js +38 -11
- package/templates/scripts/openspec/init-plan-workspace.sh +34 -3
- package/templates/vscode/guardex-active-agents/README.md +7 -6
- package/templates/vscode/guardex-active-agents/extension.js +523 -73
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +13 -3
- package/templates/vscode/guardex-active-agents/session-schema.js +311 -4
package/README.md
CHANGED
|
@@ -340,6 +340,7 @@ gx sync
|
|
|
340
340
|
```sh
|
|
341
341
|
gx agents start # review monitor + stale cleanup
|
|
342
342
|
gx agents stop
|
|
343
|
+
gx agents stop --pid 12345
|
|
343
344
|
gx agents status
|
|
344
345
|
|
|
345
346
|
# tuning
|
|
@@ -671,6 +672,11 @@ npm pack --dry-run
|
|
|
671
672
|
<details>
|
|
672
673
|
<summary><strong>v7.x</strong></summary>
|
|
673
674
|
|
|
675
|
+
### v7.0.23
|
|
676
|
+
- Bumped `@imdeadpool/guardex` from `7.0.22` to `7.0.23` so GitHub release and npm can advance together after `7.0.22` reached npm without a matching published GitHub release.
|
|
677
|
+
- Active Agents stays easier to scan and more truthful: the package repo remains the canonical source, inspect/install paths stay loadable across VS Code churn, and session rows group under worktrees with clearer merged-cleanup truth.
|
|
678
|
+
- Guardex prompt and finish guidance now pushes faster phase-based execution, keeps helper behavior single-sourced, and avoids fragmented probe loops when cleanup or branch-deletion races appear.
|
|
679
|
+
|
|
674
680
|
### v7.0.22
|
|
675
681
|
- Bumped `@imdeadpool/guardex` from `7.0.21` to `7.0.22` so npm can publish the next release from the current merged mainline.
|
|
676
682
|
- The shipped `main` payload already includes lower-token prompt slices, SCM-selected lane visibility, truthful merged-cleanup evidence, the Active Agents brand/icon refresh, and the remaining CLI extraction cleanups without changing Guardex behavior.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.23",
|
|
4
4
|
"description": "Guardian T-Rex for your multi-agent repo. Isolated worktrees, file locks, and PR-only merges stop parallel Codex & Claude agents from overwriting each other's work. Auto-wires Oh My Codex, Oh My Claude, OpenSpec, and Caveman.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -69,5 +69,9 @@
|
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"fast-check": "^3.23.2"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"jsonc-parser": "^3.3.1",
|
|
75
|
+
"semver": "^7.7.4"
|
|
72
76
|
}
|
|
73
77
|
}
|
package/src/cli/args.js
CHANGED
|
@@ -271,6 +271,7 @@ function parseAgentsArgs(rawArgs) {
|
|
|
271
271
|
reviewIntervalSeconds: 30,
|
|
272
272
|
cleanupIntervalSeconds: 60,
|
|
273
273
|
idleMinutes: DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES,
|
|
274
|
+
pid: null,
|
|
274
275
|
};
|
|
275
276
|
|
|
276
277
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -314,12 +315,28 @@ function parseAgentsArgs(rawArgs) {
|
|
|
314
315
|
index += 1;
|
|
315
316
|
continue;
|
|
316
317
|
}
|
|
318
|
+
if (arg === '--pid') {
|
|
319
|
+
const next = rest[index + 1];
|
|
320
|
+
if (!next) {
|
|
321
|
+
throw new Error('--pid requires a positive integer value');
|
|
322
|
+
}
|
|
323
|
+
const parsedValue = Number.parseInt(next, 10);
|
|
324
|
+
if (!Number.isInteger(parsedValue) || parsedValue <= 0) {
|
|
325
|
+
throw new Error('--pid must be a positive integer');
|
|
326
|
+
}
|
|
327
|
+
options.pid = parsedValue;
|
|
328
|
+
index += 1;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
317
331
|
throw new Error(`Unknown option: ${arg}`);
|
|
318
332
|
}
|
|
319
333
|
|
|
320
334
|
if (!['start', 'stop', 'status'].includes(options.subcommand)) {
|
|
321
335
|
throw new Error(`Unknown agents subcommand: ${options.subcommand}`);
|
|
322
336
|
}
|
|
337
|
+
if (options.pid !== null && options.subcommand !== 'stop') {
|
|
338
|
+
throw new Error('--pid is only supported with `gx agents stop`');
|
|
339
|
+
}
|
|
323
340
|
|
|
324
341
|
return options;
|
|
325
342
|
}
|
|
@@ -332,6 +349,15 @@ function parseReportArgs(rawArgs) {
|
|
|
332
349
|
scorecardJson: '',
|
|
333
350
|
outputDir: '',
|
|
334
351
|
date: '',
|
|
352
|
+
taskSize: '',
|
|
353
|
+
tokens: '',
|
|
354
|
+
execCount: '',
|
|
355
|
+
writeStdinCount: '',
|
|
356
|
+
completionBeforeTail: '',
|
|
357
|
+
expectedBound: '',
|
|
358
|
+
fragmentation: '',
|
|
359
|
+
finishPath: '',
|
|
360
|
+
postProof: '',
|
|
335
361
|
dryRun: false,
|
|
336
362
|
json: false,
|
|
337
363
|
};
|
|
@@ -373,6 +399,69 @@ function parseReportArgs(rawArgs) {
|
|
|
373
399
|
index += 1;
|
|
374
400
|
continue;
|
|
375
401
|
}
|
|
402
|
+
if (arg === '--task-size') {
|
|
403
|
+
const next = rawArgs[index + 1];
|
|
404
|
+
if (!next) throw new Error('--task-size requires a value');
|
|
405
|
+
options.taskSize = next;
|
|
406
|
+
index += 1;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (arg === '--tokens') {
|
|
410
|
+
const next = rawArgs[index + 1];
|
|
411
|
+
if (!next) throw new Error('--tokens requires a value');
|
|
412
|
+
options.tokens = next;
|
|
413
|
+
index += 1;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (arg === '--exec-count') {
|
|
417
|
+
const next = rawArgs[index + 1];
|
|
418
|
+
if (!next) throw new Error('--exec-count requires a value');
|
|
419
|
+
options.execCount = next;
|
|
420
|
+
index += 1;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (arg === '--write-stdin-count') {
|
|
424
|
+
const next = rawArgs[index + 1];
|
|
425
|
+
if (!next) throw new Error('--write-stdin-count requires a value');
|
|
426
|
+
options.writeStdinCount = next;
|
|
427
|
+
index += 1;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (arg === '--completion-before-tail') {
|
|
431
|
+
const next = rawArgs[index + 1];
|
|
432
|
+
if (!next) throw new Error('--completion-before-tail requires yes or no');
|
|
433
|
+
options.completionBeforeTail = next;
|
|
434
|
+
index += 1;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (arg === '--expected-bound') {
|
|
438
|
+
const next = rawArgs[index + 1];
|
|
439
|
+
if (!next) throw new Error('--expected-bound requires a value');
|
|
440
|
+
options.expectedBound = next;
|
|
441
|
+
index += 1;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (arg === '--fragmentation') {
|
|
445
|
+
const next = rawArgs[index + 1];
|
|
446
|
+
if (!next) throw new Error('--fragmentation requires a value');
|
|
447
|
+
options.fragmentation = next;
|
|
448
|
+
index += 1;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (arg === '--finish-path') {
|
|
452
|
+
const next = rawArgs[index + 1];
|
|
453
|
+
if (!next) throw new Error('--finish-path requires a value');
|
|
454
|
+
options.finishPath = next;
|
|
455
|
+
index += 1;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (arg === '--post-proof') {
|
|
459
|
+
const next = rawArgs[index + 1];
|
|
460
|
+
if (!next) throw new Error('--post-proof requires a value');
|
|
461
|
+
options.postProof = next;
|
|
462
|
+
index += 1;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
376
465
|
if (arg === '--dry-run') {
|
|
377
466
|
options.dryRun = true;
|
|
378
467
|
continue;
|
package/src/cli/main.js
CHANGED
|
@@ -5,6 +5,7 @@ const sandboxModule = require('../sandbox');
|
|
|
5
5
|
const toolchainModule = require('../toolchain');
|
|
6
6
|
const finishCommands = require('../finish');
|
|
7
7
|
const doctorModule = require('../doctor');
|
|
8
|
+
const sessionSeverityReport = require('../report/session-severity');
|
|
8
9
|
const {
|
|
9
10
|
fs,
|
|
10
11
|
path,
|
|
@@ -113,6 +114,12 @@ const {
|
|
|
113
114
|
runReviewBotCommand,
|
|
114
115
|
invokePackageAsset,
|
|
115
116
|
} = require('../core/runtime');
|
|
117
|
+
const {
|
|
118
|
+
parseVersionString,
|
|
119
|
+
compareParsedVersions,
|
|
120
|
+
isNewerVersion,
|
|
121
|
+
} = require('../core/versions');
|
|
122
|
+
const { readSingleLineFromStdin } = require('../core/stdin');
|
|
116
123
|
const {
|
|
117
124
|
normalizeManagedForcePath,
|
|
118
125
|
parseCommonArgs,
|
|
@@ -159,6 +166,7 @@ const {
|
|
|
159
166
|
ensureHookShim,
|
|
160
167
|
copyTemplateFile,
|
|
161
168
|
ensureTemplateFilePresent,
|
|
169
|
+
materializePackageRepoTemplateFiles,
|
|
162
170
|
ensureOmxScaffold,
|
|
163
171
|
ensureLockRegistry,
|
|
164
172
|
lockStateOrError,
|
|
@@ -168,9 +176,6 @@ const {
|
|
|
168
176
|
removeLegacyManagedRepoFile,
|
|
169
177
|
ensureAgentsSnippet,
|
|
170
178
|
ensureManagedGitignore,
|
|
171
|
-
stripJsonComments,
|
|
172
|
-
stripJsonTrailingCommas,
|
|
173
|
-
parseJsonObjectLikeFile,
|
|
174
179
|
buildRepoVscodeSettings,
|
|
175
180
|
ensureRepoVscodeSettings,
|
|
176
181
|
configureHooks,
|
|
@@ -866,44 +871,6 @@ function isInteractiveTerminal() {
|
|
|
866
871
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
867
872
|
}
|
|
868
873
|
|
|
869
|
-
const stdinWaitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
870
|
-
|
|
871
|
-
function sleepSyncMs(milliseconds) {
|
|
872
|
-
Atomics.wait(stdinWaitArray, 0, 0, milliseconds);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function readSingleLineFromStdin() {
|
|
876
|
-
let input = '';
|
|
877
|
-
const buffer = Buffer.alloc(1);
|
|
878
|
-
|
|
879
|
-
while (true) {
|
|
880
|
-
let bytesRead = 0;
|
|
881
|
-
try {
|
|
882
|
-
bytesRead = fs.readSync(process.stdin.fd, buffer, 0, 1);
|
|
883
|
-
} catch (error) {
|
|
884
|
-
if (error && ['EAGAIN', 'EWOULDBLOCK', 'EINTR'].includes(error.code)) {
|
|
885
|
-
sleepSyncMs(15);
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
return input;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
if (bytesRead === 0) {
|
|
892
|
-
if (process.stdin.isTTY) {
|
|
893
|
-
sleepSyncMs(15);
|
|
894
|
-
continue;
|
|
895
|
-
}
|
|
896
|
-
return input;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
const char = buffer.toString('utf8', 0, bytesRead);
|
|
900
|
-
if (char === '\n' || char === '\r') {
|
|
901
|
-
return input;
|
|
902
|
-
}
|
|
903
|
-
input += char;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
874
|
function parseAutoApproval(name) {
|
|
908
875
|
const raw = process.env[name];
|
|
909
876
|
if (raw == null) return null;
|
|
@@ -982,38 +949,6 @@ function describeGuardexRepoToggle(toggle) {
|
|
|
982
949
|
return `${toggle.source} (${GUARDEX_REPO_TOGGLE_ENV}=${toggle.raw})`;
|
|
983
950
|
}
|
|
984
951
|
|
|
985
|
-
function parseVersionString(version) {
|
|
986
|
-
const match = String(version || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
987
|
-
if (!match) return null;
|
|
988
|
-
return [
|
|
989
|
-
Number.parseInt(match[1], 10),
|
|
990
|
-
Number.parseInt(match[2], 10),
|
|
991
|
-
Number.parseInt(match[3], 10),
|
|
992
|
-
];
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
function compareParsedVersions(left, right) {
|
|
996
|
-
if (!left || !right) return 0;
|
|
997
|
-
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
998
|
-
const leftValue = left[index] || 0;
|
|
999
|
-
const rightValue = right[index] || 0;
|
|
1000
|
-
if (leftValue > rightValue) return 1;
|
|
1001
|
-
if (leftValue < rightValue) return -1;
|
|
1002
|
-
}
|
|
1003
|
-
return 0;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
function isNewerVersion(latest, current) {
|
|
1007
|
-
const latestParts = parseVersionString(latest);
|
|
1008
|
-
const currentParts = parseVersionString(current);
|
|
1009
|
-
|
|
1010
|
-
if (!latestParts || !currentParts) {
|
|
1011
|
-
return String(latest || '').trim() !== String(current || '').trim();
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
return compareParsedVersions(latestParts, currentParts) > 0;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
952
|
function parseNpmVersionOutput(stdout) {
|
|
1018
953
|
const trimmed = String(stdout || '').trim();
|
|
1019
954
|
if (!trimmed) return '';
|
|
@@ -1445,6 +1380,7 @@ function runInstallInternal(options) {
|
|
|
1445
1380
|
),
|
|
1446
1381
|
);
|
|
1447
1382
|
}
|
|
1383
|
+
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
|
|
1448
1384
|
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
1449
1385
|
for (const hookName of HOOK_NAMES) {
|
|
1450
1386
|
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
@@ -1501,6 +1437,7 @@ function runFixInternal(options) {
|
|
|
1501
1437
|
}
|
|
1502
1438
|
operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
|
|
1503
1439
|
}
|
|
1440
|
+
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
|
|
1504
1441
|
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
1505
1442
|
for (const hookName of HOOK_NAMES) {
|
|
1506
1443
|
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
@@ -2219,10 +2156,15 @@ function processAlive(pid) {
|
|
|
2219
2156
|
}
|
|
2220
2157
|
try {
|
|
2221
2158
|
process.kill(normalizedPid, 0);
|
|
2222
|
-
return true;
|
|
2223
2159
|
} catch (_error) {
|
|
2224
2160
|
return false;
|
|
2225
2161
|
}
|
|
2162
|
+
|
|
2163
|
+
const state = readProcessState(normalizedPid);
|
|
2164
|
+
if (state.startsWith('Z')) {
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
return true;
|
|
2226
2168
|
}
|
|
2227
2169
|
|
|
2228
2170
|
function sleepSeconds(seconds) {
|
|
@@ -2240,6 +2182,14 @@ function readProcessCommand(pid) {
|
|
|
2240
2182
|
return String(result.stdout || '').trim();
|
|
2241
2183
|
}
|
|
2242
2184
|
|
|
2185
|
+
function readProcessState(pid) {
|
|
2186
|
+
const result = run('ps', ['-o', 'stat=', '-p', String(pid)]);
|
|
2187
|
+
if (isSpawnFailure(result) || result.status !== 0) {
|
|
2188
|
+
return '';
|
|
2189
|
+
}
|
|
2190
|
+
return String(result.stdout || '').trim();
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2243
2193
|
function stopAgentProcessByPid(pid, expectedToken = '') {
|
|
2244
2194
|
const normalizedPid = Number.parseInt(String(pid || ''), 10);
|
|
2245
2195
|
if (!Number.isInteger(normalizedPid) || normalizedPid <= 0) {
|
|
@@ -2431,6 +2381,16 @@ function agents(rawArgs) {
|
|
|
2431
2381
|
}
|
|
2432
2382
|
|
|
2433
2383
|
if (options.subcommand === 'stop') {
|
|
2384
|
+
if (options.pid) {
|
|
2385
|
+
const stopResult = stopAgentProcessByPid(options.pid);
|
|
2386
|
+
const success = ['stopped', 'not-running'].includes(stopResult.status);
|
|
2387
|
+
console.log(
|
|
2388
|
+
`[${TOOL_NAME}] Stopped agent pid ${options.pid} (${stopResult.status}).`,
|
|
2389
|
+
);
|
|
2390
|
+
process.exitCode = success ? 0 : 1;
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2434
2394
|
const existingState = readAgentsState(repoRoot);
|
|
2435
2395
|
if (!existingState) {
|
|
2436
2396
|
console.log(`[${TOOL_NAME}] Repo agents are not running for ${repoRoot}.`);
|
|
@@ -2474,15 +2434,29 @@ function report(rawArgs) {
|
|
|
2474
2434
|
console.log(
|
|
2475
2435
|
`${TOOL_NAME} report commands:\n` +
|
|
2476
2436
|
` ${TOOL_NAME} report scorecard [--target <path>] [--repo github.com/<owner>/<repo>] [--scorecard-json <file>] [--output-dir <path>] [--date YYYY-MM-DD] [--dry-run] [--json]\n` +
|
|
2437
|
+
` ${TOOL_NAME} report session-severity --task-size <narrow-patch|medium-change|large-change> --tokens <count> --exec-count <count> --write-stdin-count <count> --completion-before-tail <yes|no> [--expected-bound <count>] [--fragmentation <preset|0-25>] [--finish-path <preset|0-15>] [--post-proof <preset|0-15>] [--json]\n` +
|
|
2477
2438
|
`\n` +
|
|
2478
2439
|
`Examples:\n` +
|
|
2479
2440
|
` ${TOOL_NAME} report scorecard --repo github.com/recodeecom/multiagent-safety\n` +
|
|
2480
|
-
` ${TOOL_NAME} report scorecard --scorecard-json ./scorecard.json --date 2026-04-10
|
|
2441
|
+
` ${TOOL_NAME} report scorecard --scorecard-json ./scorecard.json --date 2026-04-10\n` +
|
|
2442
|
+
` ${TOOL_NAME} report session-severity --task-size narrow-patch --tokens 3850000 --exec-count 18 --write-stdin-count 6 --completion-before-tail yes --fragmentation 14 --finish-path 6 --post-proof 4`,
|
|
2481
2443
|
);
|
|
2482
2444
|
process.exitCode = 0;
|
|
2483
2445
|
return;
|
|
2484
2446
|
}
|
|
2485
2447
|
|
|
2448
|
+
if (subcommand === 'session-severity') {
|
|
2449
|
+
const payload = sessionSeverityReport.buildSessionSeverityReport(options);
|
|
2450
|
+
if (options.json) {
|
|
2451
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
2452
|
+
process.exitCode = 0;
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
console.log(sessionSeverityReport.renderSessionSeverityReport(payload));
|
|
2456
|
+
process.exitCode = 0;
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2486
2460
|
if (subcommand !== 'scorecard') {
|
|
2487
2461
|
throw new Error(`Unknown report subcommand: ${subcommand}`);
|
|
2488
2462
|
}
|
package/src/context.js
CHANGED
|
@@ -128,8 +128,19 @@ const TEMPLATE_FILES = [
|
|
|
128
128
|
'vscode/guardex-active-agents/extension.js',
|
|
129
129
|
'vscode/guardex-active-agents/session-schema.js',
|
|
130
130
|
'vscode/guardex-active-agents/README.md',
|
|
131
|
+
'vscode/guardex-active-agents/icon.png',
|
|
131
132
|
];
|
|
132
133
|
|
|
134
|
+
const PACKAGE_ROOT_SOURCE_OVERRIDES = new Set([
|
|
135
|
+
'scripts/agent-session-state.js',
|
|
136
|
+
'scripts/install-vscode-active-agents-extension.js',
|
|
137
|
+
'vscode/guardex-active-agents/package.json',
|
|
138
|
+
'vscode/guardex-active-agents/extension.js',
|
|
139
|
+
'vscode/guardex-active-agents/session-schema.js',
|
|
140
|
+
'vscode/guardex-active-agents/README.md',
|
|
141
|
+
'vscode/guardex-active-agents/icon.png',
|
|
142
|
+
]);
|
|
143
|
+
|
|
133
144
|
const LEGACY_WORKFLOW_SHIM_SPECS = [
|
|
134
145
|
{ relativePath: 'scripts/agent-branch-start.sh', kind: 'shell', command: ['branch', 'start'] },
|
|
135
146
|
{ relativePath: 'scripts/agent-branch-finish.sh', kind: 'shell', command: ['branch', 'finish'] },
|
|
@@ -202,6 +213,7 @@ const PACKAGE_SCRIPT_ASSETS = {
|
|
|
202
213
|
branchMerge: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-merge.sh'),
|
|
203
214
|
codexAgent: path.join(TEMPLATE_ROOT, 'scripts', 'codex-agent.sh'),
|
|
204
215
|
reviewBot: path.join(TEMPLATE_ROOT, 'scripts', 'review-bot-watch.sh'),
|
|
216
|
+
sessionState: path.join(TEMPLATE_ROOT, 'scripts', 'agent-session-state.js'),
|
|
205
217
|
worktreePrune: path.join(TEMPLATE_ROOT, 'scripts', 'agent-worktree-prune.sh'),
|
|
206
218
|
lockTool: path.join(TEMPLATE_ROOT, 'scripts', 'agent-file-locks.py'),
|
|
207
219
|
planInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-plan-workspace.sh'),
|
|
@@ -349,7 +361,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
349
361
|
['release', 'Create or update the current GitHub release with README-generated notes'],
|
|
350
362
|
['agents', 'Start/stop repo-scoped review + cleanup bots'],
|
|
351
363
|
['prompt', 'Print AI setup checklist or named slices (--exec, --part, --list-parts, --snippet)'],
|
|
352
|
-
['report', 'Security/safety reports (e.g. OpenSSF scorecard)'],
|
|
364
|
+
['report', 'Security/safety reports (e.g. OpenSSF scorecard, session severity)'],
|
|
353
365
|
['help', 'Show this help output'],
|
|
354
366
|
['version', 'Print GitGuardex version'],
|
|
355
367
|
];
|
|
@@ -413,7 +425,8 @@ const AI_SETUP_PARTS = [
|
|
|
413
425
|
label: 'Task loop',
|
|
414
426
|
promptLines: [
|
|
415
427
|
'gx branch start "<task>" "<agent>"',
|
|
416
|
-
'then gx locks claim --branch "<agent-branch>" <file...> -> gx branch finish',
|
|
428
|
+
'then gx locks claim --branch "<agent-branch>" <file...> -> inspect once -> patch once -> verify once -> gx branch finish',
|
|
429
|
+
'batch discovery, git/PR, and CI by phase; avoid repeated peeks or stdin loops',
|
|
417
430
|
],
|
|
418
431
|
execLines: [
|
|
419
432
|
'gx branch start "<task>" "<agent>"',
|
|
@@ -619,6 +632,7 @@ module.exports = {
|
|
|
619
632
|
HOOK_NAMES,
|
|
620
633
|
toDestinationPath,
|
|
621
634
|
TEMPLATE_FILES,
|
|
635
|
+
PACKAGE_ROOT_SOURCE_OVERRIDES,
|
|
622
636
|
LEGACY_WORKFLOW_SHIM_SPECS,
|
|
623
637
|
LEGACY_WORKFLOW_SHIMS,
|
|
624
638
|
MANAGED_TEMPLATE_DESTINATIONS,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const { StringDecoder } = require('node:string_decoder');
|
|
3
|
+
|
|
4
|
+
const stdinWaitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
5
|
+
|
|
6
|
+
function sleepSyncMs(milliseconds) {
|
|
7
|
+
Atomics.wait(stdinWaitArray, 0, 0, milliseconds);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readSingleLineFromStdin(options = {}) {
|
|
11
|
+
const fsModule = options.fsModule || fs;
|
|
12
|
+
const input = options.input || process.stdin;
|
|
13
|
+
const sleepSync = options.sleepSync || sleepSyncMs;
|
|
14
|
+
const retryDelayMs = options.retryDelayMs == null ? 15 : options.retryDelayMs;
|
|
15
|
+
const buffer = Buffer.alloc(1);
|
|
16
|
+
const decoder = new StringDecoder('utf8');
|
|
17
|
+
let text = '';
|
|
18
|
+
|
|
19
|
+
while (true) {
|
|
20
|
+
let bytesRead = 0;
|
|
21
|
+
try {
|
|
22
|
+
bytesRead = fsModule.readSync(input.fd, buffer, 0, 1);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error && ['EAGAIN', 'EWOULDBLOCK', 'EINTR'].includes(error.code)) {
|
|
25
|
+
sleepSync(retryDelayMs);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
return text + decoder.end();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (bytesRead === 0) {
|
|
32
|
+
if (input.isTTY) {
|
|
33
|
+
sleepSync(retryDelayMs);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
return text + decoder.end();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const char = decoder.write(buffer.subarray(0, bytesRead));
|
|
40
|
+
if (!char) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (char === '\n' || char === '\r') {
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
text += char;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
readSingleLineFromStdin,
|
|
52
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const semver = require('semver');
|
|
2
|
+
|
|
3
|
+
function parseVersionString(version) {
|
|
4
|
+
const trimmed = String(version || '').trim();
|
|
5
|
+
if (!trimmed) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return semver.valid(trimmed) || null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function compareParsedVersions(left, right) {
|
|
12
|
+
if (!left || !right) {
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
return semver.compare(left, right);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isNewerVersion(latest, current) {
|
|
19
|
+
const latestParts = parseVersionString(latest);
|
|
20
|
+
const currentParts = parseVersionString(current);
|
|
21
|
+
|
|
22
|
+
if (!latestParts || !currentParts) {
|
|
23
|
+
return String(latest || '').trim() !== String(current || '').trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return semver.gt(latestParts, currentParts);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
parseVersionString,
|
|
31
|
+
compareParsedVersions,
|
|
32
|
+
isNewerVersion,
|
|
33
|
+
};
|
package/src/hooks/index.js
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
2
|
|
|
3
|
+
function requireFlagValue(rawArgs, index, flagName) {
|
|
4
|
+
const value = rawArgs[index + 1];
|
|
5
|
+
if (!value || value.startsWith('--')) {
|
|
6
|
+
throw new Error(`${flagName} requires a value`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseHeartbeatArgs(rawArgs) {
|
|
12
|
+
let branch = '';
|
|
13
|
+
let state = '';
|
|
14
|
+
|
|
15
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
16
|
+
const arg = rawArgs[index];
|
|
17
|
+
if (arg === '--branch') {
|
|
18
|
+
branch = requireFlagValue(rawArgs, index, '--branch');
|
|
19
|
+
index += 1;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (arg === '--state') {
|
|
23
|
+
state = requireFlagValue(rawArgs, index, '--state');
|
|
24
|
+
index += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unknown heartbeat option: ${arg}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!branch) {
|
|
31
|
+
throw new Error('heartbeat requires --branch <agent/...>');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { branch, state };
|
|
35
|
+
}
|
|
36
|
+
|
|
3
37
|
function hook(rawArgs, deps) {
|
|
4
38
|
const {
|
|
5
39
|
extractTargetedArgs,
|
|
@@ -55,6 +89,36 @@ function internal(rawArgs, deps) {
|
|
|
55
89
|
} = deps;
|
|
56
90
|
|
|
57
91
|
const [subcommand, assetKey, ...rest] = rawArgs;
|
|
92
|
+
if (subcommand === 'heartbeat') {
|
|
93
|
+
const { target, passthrough } = extractTargetedArgs([assetKey, ...rest].filter(Boolean));
|
|
94
|
+
const repoRoot = resolveRepoRoot(target);
|
|
95
|
+
const options = parseHeartbeatArgs(passthrough);
|
|
96
|
+
const heartbeatArgs = ['heartbeat', '--repo', repoRoot, '--branch', options.branch];
|
|
97
|
+
if (options.state) {
|
|
98
|
+
heartbeatArgs.push('--state', options.state);
|
|
99
|
+
}
|
|
100
|
+
const result = runPackageAsset('sessionState', heartbeatArgs, { cwd: repoRoot });
|
|
101
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
102
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
103
|
+
process.exitCode = result.status;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (subcommand === 'stop-session') {
|
|
107
|
+
const { target, passthrough } = extractTargetedArgs([assetKey, ...rest].filter(Boolean));
|
|
108
|
+
const repoRoot = resolveRepoRoot(target);
|
|
109
|
+
const options = parseHeartbeatArgs(passthrough);
|
|
110
|
+
const result = runPackageAsset('sessionState', [
|
|
111
|
+
'terminate',
|
|
112
|
+
'--repo',
|
|
113
|
+
repoRoot,
|
|
114
|
+
'--branch',
|
|
115
|
+
options.branch,
|
|
116
|
+
], { cwd: repoRoot });
|
|
117
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
118
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
119
|
+
process.exitCode = result.status;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
58
122
|
if (subcommand !== 'run-shell') {
|
|
59
123
|
throw new Error(`Unknown internal command: ${subcommand || '(missing)'}`);
|
|
60
124
|
}
|
package/src/output/index.js
CHANGED
|
@@ -87,6 +87,62 @@ function detectAutoFinishSummaryStatus(summary) {
|
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
const AUTO_FINISH_DETAIL_PRIORITY = new Map([
|
|
91
|
+
['fail', 0],
|
|
92
|
+
['pending', 1],
|
|
93
|
+
['done', 2],
|
|
94
|
+
['skip', 3],
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
function autoFinishDetailPriority(status) {
|
|
98
|
+
return AUTO_FINISH_DETAIL_PRIORITY.get(status) ?? AUTO_FINISH_DETAIL_PRIORITY.size;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sortAutoFinishDetailEntries(details) {
|
|
102
|
+
return details
|
|
103
|
+
.map((detail, index) => {
|
|
104
|
+
const status = detectAutoFinishDetailStatus(detail) || 'other';
|
|
105
|
+
return {
|
|
106
|
+
detail,
|
|
107
|
+
index,
|
|
108
|
+
status,
|
|
109
|
+
priority: autoFinishDetailPriority(status),
|
|
110
|
+
};
|
|
111
|
+
})
|
|
112
|
+
.sort((left, right) => (left.priority - right.priority) || (left.index - right.index));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function summarizeHiddenAutoFinishDetails(hiddenEntries) {
|
|
116
|
+
const counts = new Map();
|
|
117
|
+
for (const entry of hiddenEntries) {
|
|
118
|
+
counts.set(entry.status, (counts.get(entry.status) || 0) + 1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const segments = ['fail', 'pending', 'done', 'skip', 'other']
|
|
122
|
+
.map((status) => {
|
|
123
|
+
const count = counts.get(status) || 0;
|
|
124
|
+
return count > 0 ? `${status}=${count}` : '';
|
|
125
|
+
})
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
|
|
128
|
+
let status = null;
|
|
129
|
+
if ((counts.get('fail') || 0) > 0) {
|
|
130
|
+
status = 'fail';
|
|
131
|
+
} else if ((counts.get('pending') || 0) > 0) {
|
|
132
|
+
status = 'pending';
|
|
133
|
+
} else if ((counts.get('done') || 0) > 0) {
|
|
134
|
+
status = 'done';
|
|
135
|
+
} else if ((counts.get('skip') || 0) > 0) {
|
|
136
|
+
status = 'skip';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
status,
|
|
141
|
+
message: `… ${hiddenEntries.length} more branch result(s) hidden: ${segments.join(', ')}. ` +
|
|
142
|
+
'Re-run with --verbose-auto-finish for full details.',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
90
146
|
function statusDot(status) {
|
|
91
147
|
if (status === 'active') {
|
|
92
148
|
return colorize('●', '32');
|
|
@@ -360,15 +416,19 @@ function printAutoFinishSummary(summary, options = {}) {
|
|
|
360
416
|
detectAutoFinishSummaryStatus(summary),
|
|
361
417
|
),
|
|
362
418
|
);
|
|
363
|
-
const
|
|
419
|
+
const sortedDetailEntries = verbose ? [] : sortAutoFinishDetailEntries(details);
|
|
420
|
+
const visibleDetails = verbose
|
|
421
|
+
? details
|
|
422
|
+
: sortedDetailEntries.slice(0, detailLimit).map((entry) => summarizeAutoFinishDetail(entry.detail));
|
|
364
423
|
for (const detail of visibleDetails) {
|
|
365
424
|
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
|
|
366
425
|
}
|
|
367
|
-
if (!verbose &&
|
|
426
|
+
if (!verbose && sortedDetailEntries.length > visibleDetails.length) {
|
|
427
|
+
const hiddenSummary = summarizeHiddenAutoFinishDetails(sortedDetailEntries.slice(visibleDetails.length));
|
|
368
428
|
console.log(
|
|
369
429
|
colorizeDoctorOutput(
|
|
370
|
-
`[${TOOL_NAME}]
|
|
371
|
-
'warn',
|
|
430
|
+
`[${TOOL_NAME}] ${hiddenSummary.message}`,
|
|
431
|
+
hiddenSummary.status || 'warn',
|
|
372
432
|
),
|
|
373
433
|
);
|
|
374
434
|
}
|