@imdeadpool/guardex 7.0.22 → 7.0.24

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 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,16 @@ npm pack --dry-run
671
672
  <details>
672
673
  <summary><strong>v7.x</strong></summary>
673
674
 
675
+ ### v7.0.24
676
+ - Bumped `@imdeadpool/guardex` from `7.0.23` to `7.0.24` so GitHub Releases and the npm publish retry can advance together after `v7.0.23` landed on GitHub but not on npm.
677
+ - Release verification no longer loses its base ref on tag-triggered runs, so the publish workflow keeps the history it needs before packing and publish checks.
678
+ - Keep the release scoped to version and release automation metadata only; the packaged Guardex CLI payload stays aligned with the already-verified `main` branch contents.
679
+
680
+ ### v7.0.23
681
+ - 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.
682
+ - 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.
683
+ - 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.
684
+
674
685
  ### v7.0.22
675
686
  - Bumped `@imdeadpool/guardex` from `7.0.21` to `7.0.22` so npm can publish the next release from the current merged mainline.
676
687
  - 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.22",
3
+ "version": "7.0.24",
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}.`);
@@ -2471,18 +2431,37 @@ function report(rawArgs) {
2471
2431
  const options = parseReportArgs(rawArgs);
2472
2432
  const subcommand = options.subcommand || 'help';
2473
2433
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
2434
+ const sessionSeverityHelpDetails = sessionSeverityReport.renderSessionSeverityHelpDetails()
2435
+ .split('\n')
2436
+ .map((line) => ` ${line}`)
2437
+ .join('\n');
2474
2438
  console.log(
2475
2439
  `${TOOL_NAME} report commands:\n` +
2476
2440
  ` ${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` +
2441
+ ` ${sessionSeverityReport.renderSessionSeverityCommand(TOOL_NAME)}\n` +
2442
+ `${sessionSeverityHelpDetails}\n` +
2477
2443
  `\n` +
2478
2444
  `Examples:\n` +
2479
2445
  ` ${TOOL_NAME} report scorecard --repo github.com/recodeecom/multiagent-safety\n` +
2480
- ` ${TOOL_NAME} report scorecard --scorecard-json ./scorecard.json --date 2026-04-10`,
2446
+ ` ${TOOL_NAME} report scorecard --scorecard-json ./scorecard.json --date 2026-04-10\n` +
2447
+ ` ${sessionSeverityReport.renderSessionSeverityExample(TOOL_NAME)}`,
2481
2448
  );
2482
2449
  process.exitCode = 0;
2483
2450
  return;
2484
2451
  }
2485
2452
 
2453
+ if (subcommand === 'session-severity') {
2454
+ const payload = sessionSeverityReport.buildSessionSeverityReport(options);
2455
+ if (options.json) {
2456
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
2457
+ process.exitCode = 0;
2458
+ return;
2459
+ }
2460
+ console.log(sessionSeverityReport.renderSessionSeverityReport(payload));
2461
+ process.exitCode = 0;
2462
+ return;
2463
+ }
2464
+
2486
2465
  if (subcommand !== 'scorecard') {
2487
2466
  throw new Error(`Unknown report subcommand: ${subcommand}`);
2488
2467
  }
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
+ };
@@ -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
  }
@@ -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 visibleDetails = verbose ? details : details.slice(0, detailLimit).map(summarizeAutoFinishDetail);
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 && details.length > visibleDetails.length) {
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}] ${details.length - visibleDetails.length} more branch result(s). Re-run with --verbose-auto-finish for full details.`,
371
- 'warn',
430
+ `[${TOOL_NAME}] ${hiddenSummary.message}`,
431
+ hiddenSummary.status || 'warn',
372
432
  ),
373
433
  );
374
434
  }