@imdeadpool/guardex 7.0.27 → 7.0.31
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 +1 -1
- package/src/cli/main.js +279 -23
- package/src/context.js +64 -21
- package/src/doctor/index.js +42 -20
- package/src/finish/index.js +1 -0
- package/src/output/index.js +151 -15
- package/src/toolchain/index.js +6 -0
- package/templates/scripts/agent-branch-finish.sh +36 -21
- package/templates/scripts/agent-branch-start.sh +15 -1
- package/templates/scripts/codex-agent.sh +16 -1
- package/templates/vscode/guardex-active-agents/extension.js +38 -3
- package/templates/vscode/guardex-active-agents/package.json +1 -1
- package/templates/vscode/guardex-active-agents/session-schema.js +23 -5
package/README.md
CHANGED
|
@@ -708,6 +708,12 @@ npm pack --dry-run
|
|
|
708
708
|
<details>
|
|
709
709
|
<summary><strong>v7.x</strong></summary>
|
|
710
710
|
|
|
711
|
+
### v7.0.28
|
|
712
|
+
- Bumped `@imdeadpool/guardex` from `7.0.27` to `7.0.28` so the CLI help redesign can ship on a fresh npm version.
|
|
713
|
+
- `gx --help` and `gx` (no args) now render commands as a grouped catalog (Setup & health / Branch workflow / Coordination / Agents & reports / Meta) with short group descriptions, so the top of the help screen shows the newcomer path instead of a flat 20-row list.
|
|
714
|
+
- Added a three-step Quickstart block (`gx setup` → `gx branch start "<task>" "<agent>"` → `gx branch finish --via-pr --wait-for-merge --cleanup`) to both help surfaces so the intended install/setup sequence is visible before the full command reference.
|
|
715
|
+
- Exposed `CLI_COMMAND_GROUPS` and `CLI_QUICKSTART_STEPS` from `src/context.js` (with `CLI_COMMAND_DESCRIPTIONS` derived from the grouped source of truth), giving future help tooling and integrations a structured way to iterate the catalog without re-parsing flat rows.
|
|
716
|
+
|
|
711
717
|
### v7.0.27
|
|
712
718
|
- Bumped `@imdeadpool/guardex` from `7.0.26` to `7.0.27` so npm can publish a fresh version after `7.0.26` was already taken on the registry.
|
|
713
719
|
- The shipped `agent-branch-start.sh` copies now keep the startup auto-transfer path alive under `set -o pipefail`, so Guardex can still restore moved changes back to the protected checkout when branch startup hits a later failure.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.31",
|
|
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,
|
package/src/cli/main.js
CHANGED
|
@@ -146,8 +146,10 @@ const {
|
|
|
146
146
|
colorizeDoctorOutput,
|
|
147
147
|
statusDot,
|
|
148
148
|
printToolLogsSummary,
|
|
149
|
+
getInvokedCliName,
|
|
149
150
|
usage,
|
|
150
151
|
formatElapsedDuration,
|
|
152
|
+
startTransientSpinner,
|
|
151
153
|
compactAutoFinishPathSegments,
|
|
152
154
|
detectRecoverableAutoFinishConflict,
|
|
153
155
|
printAutoFinishSummary,
|
|
@@ -889,6 +891,80 @@ function parseBooleanLike(raw) {
|
|
|
889
891
|
return null;
|
|
890
892
|
}
|
|
891
893
|
|
|
894
|
+
function autoDoctorEnabledForCurrentSession() {
|
|
895
|
+
const explicit = parseBooleanLike(process.env.GUARDEX_AUTO_DOCTOR);
|
|
896
|
+
if (explicit != null) {
|
|
897
|
+
return explicit;
|
|
898
|
+
}
|
|
899
|
+
return isInteractiveTerminal();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function shouldAutoRunDoctorFromStatus(statusPayload) {
|
|
903
|
+
const repo = statusPayload?.repo || {};
|
|
904
|
+
return Boolean(
|
|
905
|
+
autoDoctorEnabledForCurrentSession()
|
|
906
|
+
&& repo.inGitRepo
|
|
907
|
+
&& repo.guardexEnabled !== false
|
|
908
|
+
&& repo.serviceStatus === 'degraded'
|
|
909
|
+
&& repo.scan
|
|
910
|
+
&& Number(repo.scan.findings || 0) > 0,
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function runCliSubprocessWithSpinner(args, options = {}) {
|
|
915
|
+
return new Promise((resolve, reject) => {
|
|
916
|
+
const spinner = options.spinnerMessage
|
|
917
|
+
? startTransientSpinner(options.spinnerMessage, {
|
|
918
|
+
prefix: options.spinnerPrefix || `[${TOOL_NAME}]`,
|
|
919
|
+
})
|
|
920
|
+
: { stop() {} };
|
|
921
|
+
const child = cp.spawn(process.execPath, [path.resolve(__filename), ...args], {
|
|
922
|
+
cwd: options.cwd || process.cwd(),
|
|
923
|
+
env: {
|
|
924
|
+
...process.env,
|
|
925
|
+
GUARDEX_AUTO_DOCTOR: '0',
|
|
926
|
+
},
|
|
927
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
const stopSpinner = () => spinner.stop();
|
|
931
|
+
child.stdout.on('data', (chunk) => {
|
|
932
|
+
stopSpinner();
|
|
933
|
+
process.stdout.write(chunk);
|
|
934
|
+
});
|
|
935
|
+
child.stderr.on('data', (chunk) => {
|
|
936
|
+
stopSpinner();
|
|
937
|
+
process.stderr.write(chunk);
|
|
938
|
+
});
|
|
939
|
+
child.on('error', (error) => {
|
|
940
|
+
stopSpinner();
|
|
941
|
+
reject(error);
|
|
942
|
+
});
|
|
943
|
+
child.on('close', (code) => {
|
|
944
|
+
stopSpinner();
|
|
945
|
+
resolve(typeof code === 'number' ? code : 1);
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function maybeAutoRunDoctorFromDefaultStatus(statusPayload) {
|
|
951
|
+
if (!shouldAutoRunDoctorFromStatus(statusPayload)) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const target = statusPayload?.repo?.target || process.cwd();
|
|
956
|
+
console.log(`[${TOOL_NAME}] Auto-repair: repo safety is degraded. Running '${SHORT_TOOL_NAME} doctor --current' now.`);
|
|
957
|
+
process.exitCode = await runCliSubprocessWithSpinner(
|
|
958
|
+
['doctor', '--target', target, '--current'],
|
|
959
|
+
{
|
|
960
|
+
cwd: target,
|
|
961
|
+
spinnerPrefix: `[${TOOL_NAME}] Auto-repair:`,
|
|
962
|
+
spinnerMessage: 'preparing doctor workspace',
|
|
963
|
+
},
|
|
964
|
+
);
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
|
|
892
968
|
function parseDotenvAssignmentValue(raw) {
|
|
893
969
|
let value = String(raw || '').trim();
|
|
894
970
|
if (!value) return '';
|
|
@@ -1672,12 +1748,62 @@ function setExitCodeFromScan(scan) {
|
|
|
1672
1748
|
process.exitCode = 0;
|
|
1673
1749
|
}
|
|
1674
1750
|
|
|
1675
|
-
function
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1751
|
+
function printStatusRepairHint(scanResult) {
|
|
1752
|
+
if (!scanResult || scanResult.guardexEnabled === false) {
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const scanHint = scanResult.errors === 0
|
|
1760
|
+
? `review warning details with '${SHORT_TOOL_NAME} scan'`
|
|
1761
|
+
: `inspect detailed findings with '${SHORT_TOOL_NAME} scan'`;
|
|
1762
|
+
console.log(
|
|
1763
|
+
`[${TOOL_NAME}] Quick fix: run '${SHORT_TOOL_NAME} doctor' to repair drift, or ${scanHint}.`,
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function countAgentWorktrees(repoRoot) {
|
|
1768
|
+
if (!repoRoot || typeof repoRoot !== 'string') return 0;
|
|
1769
|
+
const relPaths = ['.omc/agent-worktrees', '.omx/agent-worktrees'];
|
|
1770
|
+
let count = 0;
|
|
1771
|
+
for (const rel of relPaths) {
|
|
1772
|
+
try {
|
|
1773
|
+
const entries = fs.readdirSync(path.join(repoRoot, rel), { withFileTypes: true });
|
|
1774
|
+
count += entries.filter((entry) => entry.isDirectory()).length;
|
|
1775
|
+
} catch (_err) {
|
|
1776
|
+
// missing dir or permission error; not an active-agent signal
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return count;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function deriveNextStepHint({ scanResult, worktreeCount, invoked, inGitRepo }) {
|
|
1783
|
+
if (!inGitRepo) {
|
|
1784
|
+
return `${invoked} setup --target <path-to-git-repo> # initialize guardrails in a repo`;
|
|
1785
|
+
}
|
|
1786
|
+
if (!scanResult) {
|
|
1787
|
+
return `${invoked} setup # bootstrap repo guardrails`;
|
|
1788
|
+
}
|
|
1789
|
+
if (scanResult.guardexEnabled === false) {
|
|
1790
|
+
return `set GUARDEX_ON=1 in .env # re-enable guardrails, then '${invoked} doctor'`;
|
|
1791
|
+
}
|
|
1792
|
+
const branch = scanResult.branch || '';
|
|
1793
|
+
if (branch.startsWith('agent/')) {
|
|
1794
|
+
return `${invoked} branch finish --branch "${branch}" --via-pr --wait-for-merge --cleanup`;
|
|
1795
|
+
}
|
|
1796
|
+
if (worktreeCount > 0) {
|
|
1797
|
+
const plural = worktreeCount === 1 ? 'worktree' : 'worktrees';
|
|
1798
|
+
return `${invoked} finish --all # ${worktreeCount} active agent ${plural}`;
|
|
1799
|
+
}
|
|
1800
|
+
if (scanResult.errors > 0 || scanResult.warnings > 0) {
|
|
1801
|
+
return `${invoked} doctor # repair drift`;
|
|
1802
|
+
}
|
|
1803
|
+
return `${invoked} branch start "<task>" "<agent-name>" # start a sandboxed agent task`;
|
|
1804
|
+
}
|
|
1680
1805
|
|
|
1806
|
+
function collectServicesSnapshot() {
|
|
1681
1807
|
const toolchain = toolchainModule.detectGlobalToolchainPackages();
|
|
1682
1808
|
const npmServices = GLOBAL_TOOLCHAIN_PACKAGES.map((pkg) => {
|
|
1683
1809
|
const service = toolchainModule.getGlobalToolchainService(pkg);
|
|
@@ -1701,18 +1827,103 @@ function status(rawArgs) {
|
|
|
1701
1827
|
const localCompanionServices = toolchainModule.detectOptionalLocalCompanionTools().map((tool) => ({
|
|
1702
1828
|
name: tool.name,
|
|
1703
1829
|
displayName: tool.displayName || tool.name,
|
|
1830
|
+
installCommand: tool.installCommand,
|
|
1831
|
+
installArgs: Array.isArray(tool.installArgs) ? [...tool.installArgs] : [],
|
|
1704
1832
|
status: tool.status,
|
|
1705
1833
|
}));
|
|
1706
1834
|
const requiredSystemTools = toolchainModule.detectRequiredSystemTools();
|
|
1707
1835
|
const services = [
|
|
1708
1836
|
...npmServices,
|
|
1709
|
-
...localCompanionServices
|
|
1837
|
+
...localCompanionServices.map((tool) => ({
|
|
1838
|
+
name: tool.name,
|
|
1839
|
+
displayName: tool.displayName,
|
|
1840
|
+
status: tool.status,
|
|
1841
|
+
})),
|
|
1710
1842
|
...requiredSystemTools.map((tool) => ({
|
|
1711
1843
|
name: tool.name,
|
|
1712
1844
|
displayName: tool.displayName || tool.name,
|
|
1713
1845
|
status: tool.status,
|
|
1714
1846
|
})),
|
|
1715
1847
|
];
|
|
1848
|
+
return { toolchain, npmServices, localCompanionServices, requiredSystemTools, services };
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function maybePromptInstallMissingCompanions(snapshot) {
|
|
1852
|
+
if (envFlagIsTruthy(process.env.GUARDEX_SKIP_COMPANION_PROMPT)) {
|
|
1853
|
+
return { handled: false, installed: false };
|
|
1854
|
+
}
|
|
1855
|
+
const interactive = Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
|
|
1856
|
+
const autoApproval = toolchainModule.parseAutoApproval('GUARDEX_AUTO_COMPANION_APPROVAL');
|
|
1857
|
+
if (!interactive && autoApproval == null) {
|
|
1858
|
+
return { handled: false, installed: false };
|
|
1859
|
+
}
|
|
1860
|
+
if (!snapshot.toolchain.ok) {
|
|
1861
|
+
return { handled: false, installed: false };
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const missingPackages = snapshot.npmServices
|
|
1865
|
+
.filter((service) => service.status !== 'active')
|
|
1866
|
+
.map((service) => service.packageName);
|
|
1867
|
+
const missingLocalTools = snapshot.localCompanionServices.filter((tool) => tool.status !== 'active');
|
|
1868
|
+
if (missingPackages.length === 0 && missingLocalTools.length === 0) {
|
|
1869
|
+
return { handled: false, installed: false };
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const missingNames = [
|
|
1873
|
+
...missingPackages.map((pkg) => toolchainModule.formatGlobalToolchainServiceName(pkg)),
|
|
1874
|
+
...missingLocalTools.map((tool) => tool.displayName || tool.name),
|
|
1875
|
+
];
|
|
1876
|
+
console.log(`[${TOOL_NAME}] Missing companion tools: ${missingNames.join(', ')}.`);
|
|
1877
|
+
|
|
1878
|
+
const promptText = toolchainModule.buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools);
|
|
1879
|
+
const approved = interactive
|
|
1880
|
+
? toolchainModule.promptYesNoStrict(promptText)
|
|
1881
|
+
: autoApproval;
|
|
1882
|
+
|
|
1883
|
+
if (!approved) {
|
|
1884
|
+
console.log(
|
|
1885
|
+
`[${TOOL_NAME}] Skipped companion install. Set GUARDEX_SKIP_COMPANION_PROMPT=1 to silence this prompt, ` +
|
|
1886
|
+
`or run '${getInvokedCliName()} setup --install-only' later to install manually.`,
|
|
1887
|
+
);
|
|
1888
|
+
return { handled: true, installed: false };
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
const result = toolchainModule.performCompanionInstall(missingPackages, missingLocalTools);
|
|
1892
|
+
if (result.status === 'installed') {
|
|
1893
|
+
console.log(
|
|
1894
|
+
`[${TOOL_NAME}] ✅ Companion tools installed (${(result.packages || []).join(', ')}).`,
|
|
1895
|
+
);
|
|
1896
|
+
return { handled: true, installed: true };
|
|
1897
|
+
}
|
|
1898
|
+
if (result.status === 'failed') {
|
|
1899
|
+
console.log(
|
|
1900
|
+
`[${TOOL_NAME}] ⚠️ Companion install failed: ${result.reason}. ` +
|
|
1901
|
+
`Retry with '${getInvokedCliName()} setup --install-only'.`,
|
|
1902
|
+
);
|
|
1903
|
+
return { handled: true, installed: false };
|
|
1904
|
+
}
|
|
1905
|
+
return { handled: true, installed: false };
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function status(rawArgs) {
|
|
1909
|
+
const { found: verboseFlag, remaining: afterVerbose } = extractFlag(rawArgs, '--verbose');
|
|
1910
|
+
const options = parseCommonArgs(afterVerbose, {
|
|
1911
|
+
target: process.cwd(),
|
|
1912
|
+
json: false,
|
|
1913
|
+
});
|
|
1914
|
+
const forceCompact = envFlagIsTruthy(process.env.GUARDEX_COMPACT_STATUS);
|
|
1915
|
+
const forceExpand = envFlagIsTruthy(process.env.GUARDEX_VERBOSE_STATUS) || verboseFlag;
|
|
1916
|
+
const interactive = Boolean(process.stdout.isTTY);
|
|
1917
|
+
const invokedBasename = getInvokedCliName();
|
|
1918
|
+
|
|
1919
|
+
let snapshot = collectServicesSnapshot();
|
|
1920
|
+
if (!options.json) {
|
|
1921
|
+
const result = maybePromptInstallMissingCompanions(snapshot);
|
|
1922
|
+
if (result.installed) {
|
|
1923
|
+
snapshot = collectServicesSnapshot();
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
let { toolchain, npmServices, localCompanionServices, requiredSystemTools, services } = snapshot;
|
|
1716
1927
|
|
|
1717
1928
|
const targetPath = path.resolve(options.target);
|
|
1718
1929
|
const inGitRepo = isGitRepo(targetPath);
|
|
@@ -1752,18 +1963,27 @@ function status(rawArgs) {
|
|
|
1752
1963
|
if (options.json) {
|
|
1753
1964
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
1754
1965
|
process.exitCode = 0;
|
|
1755
|
-
return;
|
|
1966
|
+
return payload;
|
|
1756
1967
|
}
|
|
1757
1968
|
|
|
1969
|
+
const allServicesActive = toolchain.ok && services.every((service) => service.status === 'active');
|
|
1970
|
+
const compact = !forceExpand && (forceCompact || (interactive && allServicesActive));
|
|
1971
|
+
|
|
1758
1972
|
console.log(`[${TOOL_NAME}] CLI: ${payload.cli.runtime}`);
|
|
1759
1973
|
if (!toolchain.ok) {
|
|
1760
1974
|
console.log(`[${TOOL_NAME}] ⚠️ Could not detect global services: ${toolchain.error}`);
|
|
1761
1975
|
}
|
|
1762
1976
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1977
|
+
if (compact) {
|
|
1978
|
+
console.log(
|
|
1979
|
+
`[${TOOL_NAME}] Global services: ${services.length}/${services.length} ${statusDot('active')} active`,
|
|
1980
|
+
);
|
|
1981
|
+
} else {
|
|
1982
|
+
console.log(`[${TOOL_NAME}] Global services:`);
|
|
1983
|
+
for (const service of services) {
|
|
1984
|
+
const serviceLabel = service.displayName || service.name;
|
|
1985
|
+
console.log(` - ${statusDot(service.status)} ${serviceLabel}: ${service.status}`);
|
|
1986
|
+
}
|
|
1767
1987
|
}
|
|
1768
1988
|
const inactiveOptionalCompanions = [...npmServices, ...localCompanionServices]
|
|
1769
1989
|
.filter((service) => service.status !== 'active')
|
|
@@ -1799,8 +2019,16 @@ function status(rawArgs) {
|
|
|
1799
2019
|
console.log(
|
|
1800
2020
|
`[${TOOL_NAME}] Repo safety service: ${statusDot('inactive')} inactive (no git repository at target).`,
|
|
1801
2021
|
);
|
|
2022
|
+
const inactiveHint = deriveNextStepHint({
|
|
2023
|
+
scanResult: null,
|
|
2024
|
+
worktreeCount: 0,
|
|
2025
|
+
invoked: invokedBasename,
|
|
2026
|
+
inGitRepo,
|
|
2027
|
+
});
|
|
2028
|
+
console.log(`[${TOOL_NAME}] Next: ${inactiveHint}`);
|
|
2029
|
+
printToolLogsSummary({ invokedBasename, compact });
|
|
1802
2030
|
process.exitCode = 0;
|
|
1803
|
-
return;
|
|
2031
|
+
return payload;
|
|
1804
2032
|
}
|
|
1805
2033
|
|
|
1806
2034
|
if (scanResult.guardexEnabled === false) {
|
|
@@ -1809,9 +2037,23 @@ function status(rawArgs) {
|
|
|
1809
2037
|
);
|
|
1810
2038
|
console.log(`[${TOOL_NAME}] Repo: ${scanResult.repoRoot}`);
|
|
1811
2039
|
console.log(`[${TOOL_NAME}] Branch: ${scanResult.branch}`);
|
|
1812
|
-
|
|
2040
|
+
const worktreeCountDisabled = countAgentWorktrees(scanResult.repoRoot);
|
|
2041
|
+
if (worktreeCountDisabled > 0) {
|
|
2042
|
+
const plural = worktreeCountDisabled === 1 ? 'worktree' : 'worktrees';
|
|
2043
|
+
console.log(
|
|
2044
|
+
`[${TOOL_NAME}] ⚠ ${worktreeCountDisabled} active agent ${plural} under .omc/agent-worktrees or .omx/agent-worktrees.`,
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
const disabledHint = deriveNextStepHint({
|
|
2048
|
+
scanResult,
|
|
2049
|
+
worktreeCount: worktreeCountDisabled,
|
|
2050
|
+
invoked: invokedBasename,
|
|
2051
|
+
inGitRepo,
|
|
2052
|
+
});
|
|
2053
|
+
console.log(`[${TOOL_NAME}] Next: ${disabledHint}`);
|
|
2054
|
+
printToolLogsSummary({ invokedBasename, compact });
|
|
1813
2055
|
process.exitCode = 0;
|
|
1814
|
-
return;
|
|
2056
|
+
return payload;
|
|
1815
2057
|
}
|
|
1816
2058
|
|
|
1817
2059
|
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
@@ -1820,23 +2062,36 @@ function status(rawArgs) {
|
|
|
1820
2062
|
console.log(
|
|
1821
2063
|
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.warnings} warning(s)).`,
|
|
1822
2064
|
);
|
|
1823
|
-
console.log(`[${TOOL_NAME}] Run '${TOOL_NAME} scan' to review warning details.`);
|
|
1824
2065
|
} else if (scanResult.warnings === 0) {
|
|
1825
2066
|
console.log(
|
|
1826
2067
|
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.errors} error(s)).`,
|
|
1827
2068
|
);
|
|
1828
|
-
console.log(`[${TOOL_NAME}] Run '${TOOL_NAME} scan' for detailed findings.`);
|
|
1829
2069
|
} else {
|
|
1830
2070
|
console.log(
|
|
1831
2071
|
`[${TOOL_NAME}] Repo safety service: ${statusDot('degraded')} degraded (${scanResult.errors} error(s), ${scanResult.warnings} warning(s)).`,
|
|
1832
2072
|
);
|
|
1833
|
-
console.log(`[${TOOL_NAME}] Run '${TOOL_NAME} scan' for detailed findings.`);
|
|
1834
2073
|
}
|
|
2074
|
+
printStatusRepairHint(scanResult);
|
|
1835
2075
|
console.log(`[${TOOL_NAME}] Repo: ${scanResult.repoRoot}`);
|
|
1836
2076
|
console.log(`[${TOOL_NAME}] Branch: ${scanResult.branch}`);
|
|
1837
|
-
|
|
2077
|
+
const worktreeCountActive = countAgentWorktrees(scanResult.repoRoot);
|
|
2078
|
+
if (worktreeCountActive > 0) {
|
|
2079
|
+
const plural = worktreeCountActive === 1 ? 'worktree' : 'worktrees';
|
|
2080
|
+
console.log(
|
|
2081
|
+
`[${TOOL_NAME}] ⚠ ${worktreeCountActive} active agent ${plural} → ${invokedBasename} finish --all`,
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
const activeHint = deriveNextStepHint({
|
|
2085
|
+
scanResult,
|
|
2086
|
+
worktreeCount: worktreeCountActive,
|
|
2087
|
+
invoked: invokedBasename,
|
|
2088
|
+
inGitRepo,
|
|
2089
|
+
});
|
|
2090
|
+
console.log(`[${TOOL_NAME}] Next: ${activeHint}`);
|
|
2091
|
+
printToolLogsSummary({ invokedBasename, compact });
|
|
1838
2092
|
|
|
1839
2093
|
process.exitCode = 0;
|
|
2094
|
+
return payload;
|
|
1840
2095
|
}
|
|
1841
2096
|
|
|
1842
2097
|
function install(rawArgs) {
|
|
@@ -3246,13 +3501,14 @@ function protect(rawArgs) {
|
|
|
3246
3501
|
throw new Error(`Unknown protect subcommand: ${subcommand}`);
|
|
3247
3502
|
}
|
|
3248
3503
|
|
|
3249
|
-
function main() {
|
|
3504
|
+
async function main() {
|
|
3250
3505
|
const args = process.argv.slice(2);
|
|
3251
3506
|
|
|
3252
3507
|
if (args.length === 0) {
|
|
3253
3508
|
toolchainModule.maybeSelfUpdateBeforeStatus();
|
|
3254
3509
|
toolchainModule.maybeOpenSpecUpdateBeforeStatus();
|
|
3255
|
-
status([]);
|
|
3510
|
+
const statusPayload = status([]);
|
|
3511
|
+
await maybeAutoRunDoctorFromDefaultStatus(statusPayload);
|
|
3256
3512
|
return;
|
|
3257
3513
|
}
|
|
3258
3514
|
|
|
@@ -3322,9 +3578,9 @@ function main() {
|
|
|
3322
3578
|
throw new Error(`Unknown command: ${command}`);
|
|
3323
3579
|
}
|
|
3324
3580
|
|
|
3325
|
-
function runFromBin() {
|
|
3581
|
+
async function runFromBin() {
|
|
3326
3582
|
try {
|
|
3327
|
-
main();
|
|
3583
|
+
await main();
|
|
3328
3584
|
} catch (error) {
|
|
3329
3585
|
console.error(`[${TOOL_NAME}] ${error.message}`);
|
|
3330
3586
|
process.exitCode = 1;
|
|
@@ -3332,7 +3588,7 @@ function runFromBin() {
|
|
|
3332
3588
|
}
|
|
3333
3589
|
|
|
3334
3590
|
if (require.main === module) {
|
|
3335
|
-
runFromBin();
|
|
3591
|
+
void runFromBin();
|
|
3336
3592
|
}
|
|
3337
3593
|
|
|
3338
3594
|
module.exports = {
|
package/src/context.js
CHANGED
|
@@ -363,27 +363,68 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
363
363
|
'print-agents-snippet',
|
|
364
364
|
'release',
|
|
365
365
|
];
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
366
|
+
// CLI_COMMAND_GROUPS is the grouped source of truth the `gx --help` /
|
|
367
|
+
// `gx` no-args renderer uses. Each group is ordered roughly by how often a
|
|
368
|
+
// user reaches for it so the help screen answers "what do I run first?"
|
|
369
|
+
// before "what else can this do?". CLI_COMMAND_DESCRIPTIONS preserves the
|
|
370
|
+
// flat export for callers that still want the ungrouped list.
|
|
371
|
+
const CLI_COMMAND_GROUPS = [
|
|
372
|
+
{
|
|
373
|
+
label: 'Setup & health',
|
|
374
|
+
description: 'Install, repair, and check a repo. Run these first on a new clone.',
|
|
375
|
+
commands: [
|
|
376
|
+
['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target, --current)'],
|
|
377
|
+
['doctor', 'Repair drift + verify (flags: --target, --current; auto-sandboxes on protected main)'],
|
|
378
|
+
['status', 'Show GitGuardex CLI + service health without modifying files'],
|
|
379
|
+
['migrate', 'Convert legacy repo-local installs to the zero-copy CLI-owned surface'],
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
label: 'Branch workflow',
|
|
384
|
+
description: 'The sandbox → commit → PR → merge loop for agent-owned branches.',
|
|
385
|
+
commands: [
|
|
386
|
+
['branch', 'CLI-owned branch workflow surface (start/finish/merge)'],
|
|
387
|
+
['finish', 'Commit + PR + merge completed agent branches (--all, --branch)'],
|
|
388
|
+
['merge', 'Create/reuse an integration lane and merge overlapping agent branches'],
|
|
389
|
+
['sync', 'Sync agent branches with origin/<base>'],
|
|
390
|
+
['cleanup', 'Prune merged/stale agent branches and worktrees'],
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
label: 'Coordination',
|
|
395
|
+
description: 'File locks, worktrees, hooks, and protected-branch policy.',
|
|
396
|
+
commands: [
|
|
397
|
+
['locks', 'CLI-owned file lock surface (claim/allow-delete/release/status/validate)'],
|
|
398
|
+
['worktree', 'CLI-owned worktree cleanup surface (prune)'],
|
|
399
|
+
['hook', 'Hook dispatch/install surface used by managed shims'],
|
|
400
|
+
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
label: 'Agents & reports',
|
|
405
|
+
description: 'Review / cleanup bots, AI setup prompts, and safety reports.',
|
|
406
|
+
commands: [
|
|
407
|
+
['agents', 'Start/stop repo-scoped review + cleanup bots'],
|
|
408
|
+
['install-agent-skills', 'Install Guardex Codex/Claude skills into the user home'],
|
|
409
|
+
['prompt', 'Print AI setup checklist or named slices (--exec, --part, --list-parts, --snippet)'],
|
|
410
|
+
['report', 'Security/safety reports (e.g. OpenSSF scorecard, session severity)'],
|
|
411
|
+
['release', 'Create or update the current GitHub release with README-generated notes'],
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
label: 'Meta',
|
|
416
|
+
description: 'Version + help.',
|
|
417
|
+
commands: [
|
|
418
|
+
['help', 'Show this help output'],
|
|
419
|
+
['version', 'Print GitGuardex version'],
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
];
|
|
423
|
+
const CLI_COMMAND_DESCRIPTIONS = CLI_COMMAND_GROUPS.flatMap((group) => group.commands);
|
|
424
|
+
const CLI_QUICKSTART_STEPS = [
|
|
425
|
+
'gx setup',
|
|
426
|
+
'gx branch start "<task>" "<agent>"',
|
|
427
|
+
'gx branch finish --via-pr --wait-for-merge --cleanup',
|
|
387
428
|
];
|
|
388
429
|
const DEPRECATED_COMMAND_ALIASES = new Map([
|
|
389
430
|
['init', { target: 'setup', hint: 'gx setup' }],
|
|
@@ -686,6 +727,8 @@ module.exports = {
|
|
|
686
727
|
COMMAND_TYPO_ALIASES,
|
|
687
728
|
SUGGESTIBLE_COMMANDS,
|
|
688
729
|
CLI_COMMAND_DESCRIPTIONS,
|
|
730
|
+
CLI_COMMAND_GROUPS,
|
|
731
|
+
CLI_QUICKSTART_STEPS,
|
|
689
732
|
DEPRECATED_COMMAND_ALIASES,
|
|
690
733
|
AGENT_BOT_DESCRIPTIONS,
|
|
691
734
|
DOCTOR_AUTO_FINISH_DETAIL_LIMIT,
|
package/src/doctor/index.js
CHANGED
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
} = require('../sandbox');
|
|
32
32
|
const { ensureOmxScaffold, configureHooks } = require('../scaffold');
|
|
33
33
|
const { detectRecoverableAutoFinishConflict, printAutoFinishSummary } = require('../output');
|
|
34
|
+
const { autoCommitWorktreeForFinish } = require('../finish');
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* @typedef {Object} SandboxMetadata
|
|
@@ -887,23 +888,25 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
887
888
|
return summary;
|
|
888
889
|
}
|
|
889
890
|
|
|
890
|
-
|
|
891
|
-
summary.enabled = false;
|
|
892
|
-
summary.details.push('Skipped auto-finish sweep (origin remote missing).');
|
|
893
|
-
return summary;
|
|
894
|
-
}
|
|
891
|
+
const originAvailable = hasOriginRemote(repoRoot);
|
|
895
892
|
const explicitGhBin = Boolean(String(process.env.GUARDEX_GH_BIN || '').trim());
|
|
896
|
-
if (!explicitGhBin && !originRemoteLooksLikeGithub(repoRoot)) {
|
|
897
|
-
summary.enabled = false;
|
|
898
|
-
summary.details.push('Skipped auto-finish sweep (origin remote is not GitHub).');
|
|
899
|
-
return summary;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
893
|
const ghBin = process.env.GUARDEX_GH_BIN || 'gh';
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
894
|
+
const ghAvailable =
|
|
895
|
+
originAvailable &&
|
|
896
|
+
(explicitGhBin || originRemoteLooksLikeGithub(repoRoot)) &&
|
|
897
|
+
run(ghBin, ['--version']).status === 0;
|
|
898
|
+
|
|
899
|
+
let fallbackMode = '';
|
|
900
|
+
if (!originAvailable) {
|
|
901
|
+
fallbackMode = 'local';
|
|
902
|
+
summary.details.push('origin remote missing; falling back to local direct merge (no push, no PR).');
|
|
903
|
+
} else if (!ghAvailable) {
|
|
904
|
+
fallbackMode = 'direct';
|
|
905
|
+
if (!explicitGhBin && !originRemoteLooksLikeGithub(repoRoot)) {
|
|
906
|
+
summary.details.push('origin remote is not GitHub; falling back to direct merge + push.');
|
|
907
|
+
} else {
|
|
908
|
+
summary.details.push(`${ghBin} not available; falling back to direct merge + push.`);
|
|
909
|
+
}
|
|
907
910
|
}
|
|
908
911
|
|
|
909
912
|
const branchWorktrees = mapWorktreePathsByBranch(repoRoot);
|
|
@@ -936,16 +939,29 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
936
939
|
continue;
|
|
937
940
|
}
|
|
938
941
|
|
|
942
|
+
const branchWorktree = branchWorktrees.get(branch) || '';
|
|
943
|
+
if (branchWorktree && hasSignificantWorkingTreeChanges(branchWorktree)) {
|
|
944
|
+
try {
|
|
945
|
+
const commitResult = autoCommitWorktreeForFinish(repoRoot, branchWorktree, branch, {});
|
|
946
|
+
if (commitResult.committed) {
|
|
947
|
+
counts = aheadBehind(repoRoot, branch, baseBranch);
|
|
948
|
+
}
|
|
949
|
+
} catch (error) {
|
|
950
|
+
summary.failed += 1;
|
|
951
|
+
summary.details.push(`[fail] ${branch}: auto-commit failed (${error.message}).`);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
939
956
|
if (counts.ahead <= 0) {
|
|
940
957
|
summary.skipped += 1;
|
|
941
958
|
summary.details.push(`[skip] ${branch}: already merged into ${baseBranch}.`);
|
|
942
959
|
continue;
|
|
943
960
|
}
|
|
944
961
|
|
|
945
|
-
const branchWorktree = branchWorktrees.get(branch) || '';
|
|
946
962
|
if (branchWorktree && hasSignificantWorkingTreeChanges(branchWorktree)) {
|
|
947
963
|
summary.skipped += 1;
|
|
948
|
-
summary.details.push(`[skip] ${branch}: dirty worktree (${branchWorktree}).`);
|
|
964
|
+
summary.details.push(`[skip] ${branch}: dirty worktree after auto-commit (${branchWorktree}).`);
|
|
949
965
|
continue;
|
|
950
966
|
}
|
|
951
967
|
|
|
@@ -955,10 +971,16 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
955
971
|
branch,
|
|
956
972
|
'--base',
|
|
957
973
|
baseBranch,
|
|
958
|
-
'--via-pr',
|
|
959
|
-
waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
960
|
-
'--cleanup',
|
|
961
974
|
];
|
|
975
|
+
if (fallbackMode === 'local') {
|
|
976
|
+
finishArgs.push('--direct-only', '--no-push');
|
|
977
|
+
} else if (fallbackMode === 'direct') {
|
|
978
|
+
finishArgs.push('--direct-only');
|
|
979
|
+
} else {
|
|
980
|
+
finishArgs.push('--via-pr');
|
|
981
|
+
}
|
|
982
|
+
finishArgs.push(waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge');
|
|
983
|
+
finishArgs.push('--cleanup');
|
|
962
984
|
const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot });
|
|
963
985
|
const combinedOutput = [finishResult.stdout || '', finishResult.stderr || ''].join('\n').trim();
|
|
964
986
|
|
package/src/finish/index.js
CHANGED
package/src/output/index.js
CHANGED
|
@@ -6,6 +6,8 @@ const {
|
|
|
6
6
|
LEGACY_NAMES,
|
|
7
7
|
GUARDEX_REPO_TOGGLE_ENV,
|
|
8
8
|
CLI_COMMAND_DESCRIPTIONS,
|
|
9
|
+
CLI_COMMAND_GROUPS,
|
|
10
|
+
CLI_QUICKSTART_STEPS,
|
|
9
11
|
AGENT_BOT_DESCRIPTIONS,
|
|
10
12
|
DOCTOR_AUTO_FINISH_DETAIL_LIMIT,
|
|
11
13
|
DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX,
|
|
@@ -166,6 +168,41 @@ function commandCatalogLines(indent = ' ') {
|
|
|
166
168
|
);
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
// groupedCommandCatalogLines renders CLI_COMMAND_GROUPS as a nested list with
|
|
172
|
+
// group headers separated by blank lines. It accepts an optional `colorize`
|
|
173
|
+
// callback so the caller can decide whether to decorate the group label (tty
|
|
174
|
+
// mode) or leave it plain (non-tty / NO_COLOR). Returns an array of lines;
|
|
175
|
+
// `null` entries mean "emit a blank line" so tree renderers can echo pipe
|
|
176
|
+
// characters on the separator rows.
|
|
177
|
+
function groupedCommandCatalogLines(indent = ' ', options = {}) {
|
|
178
|
+
const colorizeLabel = typeof options.colorizeLabel === 'function'
|
|
179
|
+
? options.colorizeLabel
|
|
180
|
+
: (text) => text;
|
|
181
|
+
const maxCommandLength = CLI_COMMAND_DESCRIPTIONS.reduce(
|
|
182
|
+
(max, [command]) => Math.max(max, command.length),
|
|
183
|
+
0,
|
|
184
|
+
);
|
|
185
|
+
const lines = [];
|
|
186
|
+
for (let groupIndex = 0; groupIndex < CLI_COMMAND_GROUPS.length; groupIndex += 1) {
|
|
187
|
+
const group = CLI_COMMAND_GROUPS[groupIndex];
|
|
188
|
+
const header = group.description
|
|
189
|
+
? `${colorizeLabel(group.label)} — ${group.description}`
|
|
190
|
+
: colorizeLabel(group.label);
|
|
191
|
+
lines.push(`${indent}${header}`);
|
|
192
|
+
for (const [command, description] of group.commands) {
|
|
193
|
+
lines.push(`${indent} ${command.padEnd(maxCommandLength + 2)}${description}`);
|
|
194
|
+
}
|
|
195
|
+
if (groupIndex < CLI_COMMAND_GROUPS.length - 1) {
|
|
196
|
+
lines.push(null);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return lines;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function quickstartLines(indent = ' ') {
|
|
203
|
+
return CLI_QUICKSTART_STEPS.map((step, index) => `${indent}${index + 1}. ${step}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
169
206
|
function agentBotCatalogLines(indent = ' ') {
|
|
170
207
|
const maxCommandLength = AGENT_BOT_DESCRIPTIONS.reduce(
|
|
171
208
|
(max, [command]) => Math.max(max, command.length),
|
|
@@ -182,19 +219,43 @@ function repoToggleLines(indent = ' ') {
|
|
|
182
219
|
];
|
|
183
220
|
}
|
|
184
221
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
222
|
+
const KNOWN_CLI_BASENAMES = new Set(['gx', 'gitguardex', 'guardex']);
|
|
223
|
+
|
|
224
|
+
function getInvokedCliName() {
|
|
225
|
+
const raw = path.basename(String(process.argv[1] || '')).replace(/\.js$/, '');
|
|
226
|
+
if (!KNOWN_CLI_BASENAMES.has(raw)) {
|
|
227
|
+
return SHORT_TOOL_NAME;
|
|
228
|
+
}
|
|
229
|
+
return raw;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function printToolLogsSummary(options = {}) {
|
|
233
|
+
const invoked = options.invokedBasename || getInvokedCliName();
|
|
234
|
+
const compact = Boolean(options.compact);
|
|
235
|
+
|
|
236
|
+
if (compact) {
|
|
237
|
+
const helpLine = `Try '${invoked} help' for commands, or '${invoked} status --verbose' for full service details.`;
|
|
238
|
+
console.log(`[${TOOL_NAME}] ${colorize(helpLine, '2')}`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const usageLine = ` $ ${invoked} <command> [options]`;
|
|
243
|
+
const quickstartDetails = quickstartLines(' ');
|
|
188
244
|
const agentBotDetails = agentBotCatalogLines(' ');
|
|
189
245
|
const repoToggleDetails = repoToggleLines(' ');
|
|
190
246
|
|
|
191
247
|
if (!supportsAnsiColors()) {
|
|
192
|
-
|
|
248
|
+
const commandDetails = groupedCommandCatalogLines(' ');
|
|
249
|
+
console.log(`${invoked} help:`);
|
|
193
250
|
console.log(' USAGE');
|
|
194
251
|
console.log(usageLine);
|
|
252
|
+
console.log(' QUICKSTART');
|
|
253
|
+
for (const line of quickstartDetails) {
|
|
254
|
+
console.log(line);
|
|
255
|
+
}
|
|
195
256
|
console.log(' COMMANDS');
|
|
196
257
|
for (const line of commandDetails) {
|
|
197
|
-
console.log(line);
|
|
258
|
+
console.log(line ?? '');
|
|
198
259
|
}
|
|
199
260
|
console.log(' AGENT BOT');
|
|
200
261
|
for (const line of agentBotDetails) {
|
|
@@ -204,24 +265,33 @@ function printToolLogsSummary() {
|
|
|
204
265
|
for (const line of repoToggleDetails) {
|
|
205
266
|
console.log(line);
|
|
206
267
|
}
|
|
268
|
+
console.log(` Try '${invoked} doctor' for one-step repair + verification.`);
|
|
207
269
|
return;
|
|
208
270
|
}
|
|
209
271
|
|
|
210
|
-
const title = colorize(`${
|
|
272
|
+
const title = colorize(`${invoked} help`, '1;36');
|
|
211
273
|
const usageHeader = colorize('USAGE', '1');
|
|
274
|
+
const quickstartHeader = colorize('QUICKSTART', '1');
|
|
212
275
|
const commandsHeader = colorize('COMMANDS', '1');
|
|
213
276
|
const agentBotHeader = colorize('AGENT BOT', '1');
|
|
214
277
|
const repoToggleHeader = colorize('REPO TOGGLE', '1');
|
|
215
278
|
const pipe = colorize('│', '90');
|
|
216
279
|
const tee = colorize('├', '90');
|
|
217
280
|
const corner = colorize('└', '90');
|
|
281
|
+
const commandDetails = groupedCommandCatalogLines(' ', {
|
|
282
|
+
colorizeLabel: (text) => colorize(text, '1;36'),
|
|
283
|
+
});
|
|
218
284
|
|
|
219
285
|
console.log(`${title}:`);
|
|
220
286
|
console.log(` ${tee}─ ${usageHeader}`);
|
|
221
287
|
console.log(` ${pipe}${usageLine}`);
|
|
288
|
+
console.log(` ${tee}─ ${quickstartHeader}`);
|
|
289
|
+
for (const line of quickstartDetails) {
|
|
290
|
+
console.log(` ${pipe}${line.slice(2)}`);
|
|
291
|
+
}
|
|
222
292
|
console.log(` ${tee}─ ${commandsHeader}`);
|
|
223
293
|
for (const line of commandDetails) {
|
|
224
|
-
if (
|
|
294
|
+
if (line == null) {
|
|
225
295
|
console.log(` ${pipe}`);
|
|
226
296
|
continue;
|
|
227
297
|
}
|
|
@@ -243,11 +313,18 @@ function printToolLogsSummary() {
|
|
|
243
313
|
}
|
|
244
314
|
console.log(` ${pipe}${line.slice(2)}`);
|
|
245
315
|
}
|
|
246
|
-
console.log(` ${corner}─ ${colorize(`Try '${
|
|
316
|
+
console.log(` ${corner}─ ${colorize(`Try '${invoked} doctor' for one-step repair + verification.`, '2')}`);
|
|
247
317
|
}
|
|
248
318
|
|
|
249
319
|
function usage(options = {}) {
|
|
250
320
|
const { outsideGitRepo = false } = options;
|
|
321
|
+
const invoked = options.invokedBasename || getInvokedCliName();
|
|
322
|
+
|
|
323
|
+
const groupedCommandLines = groupedCommandCatalogLines(' ', {
|
|
324
|
+
colorizeLabel: (text) => colorize(text, '1;36'),
|
|
325
|
+
})
|
|
326
|
+
.map((line) => (line == null ? '' : line))
|
|
327
|
+
.join('\n');
|
|
251
328
|
|
|
252
329
|
console.log(`A command-line tool that sets up hardened multi-agent safety for git repositories.
|
|
253
330
|
|
|
@@ -255,10 +332,13 @@ VERSION
|
|
|
255
332
|
${runtimeVersion()}
|
|
256
333
|
|
|
257
334
|
USAGE
|
|
258
|
-
$ ${
|
|
335
|
+
$ ${invoked} <command> [options]
|
|
336
|
+
|
|
337
|
+
QUICKSTART
|
|
338
|
+
${quickstartLines().join('\n')}
|
|
259
339
|
|
|
260
340
|
COMMANDS
|
|
261
|
-
${
|
|
341
|
+
${groupedCommandLines}
|
|
262
342
|
|
|
263
343
|
AGENT BOT
|
|
264
344
|
${agentBotCatalogLines().join('\n')}
|
|
@@ -267,19 +347,20 @@ REPO TOGGLE
|
|
|
267
347
|
${repoToggleLines().join('\n')}
|
|
268
348
|
|
|
269
349
|
NOTES
|
|
270
|
-
- No command = ${
|
|
350
|
+
- No command = ${invoked} status (compact in a TTY; pass --verbose for full services + help tree).
|
|
351
|
+
- ${invoked} init is an alias of ${invoked} setup.
|
|
271
352
|
- Global installs need Y/N approval; GitHub CLI (gh) is required for PR automation.
|
|
272
|
-
- Target another repo: ${
|
|
353
|
+
- Target another repo: ${invoked} <cmd> --target <repo-path>.
|
|
273
354
|
- On protected main, setup/install/fix/doctor auto-sandbox via agent branch + PR flow.
|
|
274
|
-
- Run '${
|
|
355
|
+
- Run '${invoked} cleanup' to prune merged agent branches/worktrees.
|
|
275
356
|
- Legacy aliases: ${LEGACY_NAMES.join(', ')}.`);
|
|
276
357
|
|
|
277
358
|
if (outsideGitRepo) {
|
|
278
359
|
console.log(`
|
|
279
360
|
[${TOOL_NAME}] No git repository detected in current directory.
|
|
280
361
|
[${TOOL_NAME}] Start from a repo root, or pass an explicit target:
|
|
281
|
-
${
|
|
282
|
-
${
|
|
362
|
+
${invoked} setup --target <path-to-git-repo>
|
|
363
|
+
${invoked} doctor --target <path-to-git-repo>`);
|
|
283
364
|
}
|
|
284
365
|
}
|
|
285
366
|
|
|
@@ -294,6 +375,59 @@ function formatElapsedDuration(ms) {
|
|
|
294
375
|
return `${Math.round(durationMs / 1000)}s`;
|
|
295
376
|
}
|
|
296
377
|
|
|
378
|
+
function startTransientSpinner(message, options = {}) {
|
|
379
|
+
const stream = options.stream || process.stdout;
|
|
380
|
+
if (!stream || !stream.isTTY || typeof stream.write !== 'function') {
|
|
381
|
+
return {
|
|
382
|
+
stop() {},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const frames = supportsAnsiColors()
|
|
387
|
+
? ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
388
|
+
: ['-', '\\', '|', '/'];
|
|
389
|
+
const intervalMs = Number.isFinite(options.intervalMs) ? Math.max(60, options.intervalMs) : 80;
|
|
390
|
+
const prefix = String(options.prefix || `[${TOOL_NAME}]`).trim();
|
|
391
|
+
const text = String(message || '').trim();
|
|
392
|
+
let frameIndex = 0;
|
|
393
|
+
let stopped = false;
|
|
394
|
+
|
|
395
|
+
const render = () => {
|
|
396
|
+
const frame = frames[frameIndex % frames.length];
|
|
397
|
+
frameIndex += 1;
|
|
398
|
+
const indicator = supportsAnsiColors() ? colorize(frame, '36') : frame;
|
|
399
|
+
stream.write(`\r${prefix} ${indicator} ${text}`);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const clear = () => {
|
|
403
|
+
stream.write('\r');
|
|
404
|
+
if (typeof stream.clearLine === 'function') {
|
|
405
|
+
stream.clearLine(0);
|
|
406
|
+
}
|
|
407
|
+
if (typeof stream.cursorTo === 'function') {
|
|
408
|
+
stream.cursorTo(0);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
render();
|
|
413
|
+
const timer = setInterval(render, intervalMs);
|
|
414
|
+
if (typeof timer.unref === 'function') {
|
|
415
|
+
timer.unref();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
stop(finalLine = '') {
|
|
420
|
+
if (stopped) return;
|
|
421
|
+
stopped = true;
|
|
422
|
+
clearInterval(timer);
|
|
423
|
+
clear();
|
|
424
|
+
if (finalLine) {
|
|
425
|
+
stream.write(`${finalLine}\n`);
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
297
431
|
function truncateMiddle(value, maxLength) {
|
|
298
432
|
const text = String(value || '');
|
|
299
433
|
const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
|
|
@@ -454,8 +588,10 @@ module.exports = {
|
|
|
454
588
|
agentBotCatalogLines,
|
|
455
589
|
repoToggleLines,
|
|
456
590
|
printToolLogsSummary,
|
|
591
|
+
getInvokedCliName,
|
|
457
592
|
usage,
|
|
458
593
|
formatElapsedDuration,
|
|
594
|
+
startTransientSpinner,
|
|
459
595
|
truncateMiddle,
|
|
460
596
|
truncateTail,
|
|
461
597
|
compactAutoFinishPathSegments,
|
package/src/toolchain/index.js
CHANGED
|
@@ -545,6 +545,10 @@ function installGlobalToolchain(options) {
|
|
|
545
545
|
};
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
+
return performCompanionInstall(missingPackages, missingLocalTools);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function performCompanionInstall(missingPackages, missingLocalTools) {
|
|
548
552
|
const installed = [];
|
|
549
553
|
if (missingPackages.length > 0) {
|
|
550
554
|
console.log(
|
|
@@ -593,6 +597,7 @@ module.exports = {
|
|
|
593
597
|
formatGlobalToolchainServiceName,
|
|
594
598
|
describeMissingGlobalDependencyWarnings,
|
|
595
599
|
describeCompanionInstallCommands,
|
|
600
|
+
buildMissingCompanionInstallPrompt,
|
|
596
601
|
detectGlobalToolchainPackages,
|
|
597
602
|
detectRequiredSystemTools,
|
|
598
603
|
detectOptionalLocalCompanionTools,
|
|
@@ -600,4 +605,5 @@ module.exports = {
|
|
|
600
605
|
maybeSelfUpdateBeforeStatus,
|
|
601
606
|
maybeOpenSpecUpdateBeforeStatus,
|
|
602
607
|
installGlobalToolchain,
|
|
608
|
+
performCompanionInstall,
|
|
603
609
|
};
|
|
@@ -389,30 +389,45 @@ if [[ "$MERGE_MODE" == "pr" && "$PUSH_ENABLED" -eq 1 ]]; then
|
|
|
389
389
|
fi
|
|
390
390
|
|
|
391
391
|
if [[ "$should_create_integration_helper" -eq 1 ]]; then
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
integration_branch="$integration_branch_base"
|
|
397
|
-
integration_suffix=1
|
|
398
|
-
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
|
|
399
|
-
integration_worktree="${integration_worktree_base}-${integration_suffix}"
|
|
400
|
-
integration_branch="${integration_branch_base}_${integration_suffix}"
|
|
401
|
-
integration_suffix=$((integration_suffix + 1))
|
|
402
|
-
done
|
|
403
|
-
mkdir -p "$(dirname "$integration_worktree")"
|
|
392
|
+
existing_base_worktree=""
|
|
393
|
+
if [[ "$PUSH_ENABLED" -eq 0 ]]; then
|
|
394
|
+
existing_base_worktree="$(get_worktree_for_branch "$BASE_BRANCH")"
|
|
395
|
+
fi
|
|
404
396
|
|
|
405
|
-
|
|
406
|
-
|
|
397
|
+
if [[ -n "$existing_base_worktree" ]] && is_clean_worktree "$existing_base_worktree"; then
|
|
398
|
+
if ! git -C "$existing_base_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
|
|
399
|
+
echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
|
|
400
|
+
git -C "$existing_base_worktree" merge --abort >/dev/null 2>&1 || true
|
|
401
|
+
exit 1
|
|
402
|
+
fi
|
|
403
|
+
merge_completed=1
|
|
404
|
+
merge_status="direct"
|
|
405
|
+
else
|
|
406
|
+
integration_stamp="$(date +%Y%m%d-%H%M%S)"
|
|
407
|
+
integration_worktree_base="${temp_worktree_root}/__integrate-${BASE_BRANCH//\//__}-${integration_stamp}"
|
|
408
|
+
integration_branch_base="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
|
|
409
|
+
integration_worktree="$integration_worktree_base"
|
|
410
|
+
integration_branch="$integration_branch_base"
|
|
411
|
+
integration_suffix=1
|
|
412
|
+
while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
|
|
413
|
+
integration_worktree="${integration_worktree_base}-${integration_suffix}"
|
|
414
|
+
integration_branch="${integration_branch_base}_${integration_suffix}"
|
|
415
|
+
integration_suffix=$((integration_suffix + 1))
|
|
416
|
+
done
|
|
417
|
+
mkdir -p "$(dirname "$integration_worktree")"
|
|
418
|
+
|
|
419
|
+
git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
|
|
420
|
+
git -C "$integration_worktree" checkout -b "$integration_branch" >/dev/null
|
|
421
|
+
|
|
422
|
+
if ! git -C "$integration_worktree" merge --no-ff --no-edit "$SOURCE_BRANCH"; then
|
|
423
|
+
echo "[agent-branch-finish] Merge conflict detected while merging '${SOURCE_BRANCH}' into '${BASE_BRANCH}'." >&2
|
|
424
|
+
git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
|
|
425
|
+
exit 1
|
|
426
|
+
fi
|
|
407
427
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
git -C "$integration_worktree" merge --abort >/dev/null 2>&1 || true
|
|
411
|
-
exit 1
|
|
428
|
+
merge_completed=1
|
|
429
|
+
merge_status="direct"
|
|
412
430
|
fi
|
|
413
|
-
|
|
414
|
-
merge_completed=1
|
|
415
|
-
merge_status="direct"
|
|
416
431
|
fi
|
|
417
432
|
|
|
418
433
|
is_local_branch_delete_error() {
|
|
@@ -340,16 +340,30 @@ resolve_openspec_capability_slug() {
|
|
|
340
340
|
sanitize_slug "$task_slug" "general-behavior"
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
resolve_repo_prefix() {
|
|
344
|
+
local root
|
|
345
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
346
|
+
basename "$root"
|
|
347
|
+
}
|
|
348
|
+
|
|
343
349
|
resolve_worktree_leaf() {
|
|
344
350
|
local branch_name="$1"
|
|
345
351
|
local agent_slug="$2"
|
|
346
352
|
local masterplan_label=""
|
|
347
353
|
local branch_leaf=""
|
|
354
|
+
local repo_prefix
|
|
355
|
+
repo_prefix="$(resolve_repo_prefix)"
|
|
348
356
|
|
|
349
357
|
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
350
358
|
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" == "agent/${agent_slug}/"* ]]; then
|
|
351
359
|
branch_leaf="${branch_name#agent/${agent_slug}/}"
|
|
352
|
-
printf '
|
|
360
|
+
printf '%s__%s__%s__%s' "$repo_prefix" "$agent_slug" "$masterplan_label" "$branch_leaf"
|
|
361
|
+
return 0
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
if [[ "$branch_name" == agent/*/* ]]; then
|
|
365
|
+
local without_agent="${branch_name#agent/}"
|
|
366
|
+
printf '%s__%s' "$repo_prefix" "${without_agent//\//__}"
|
|
353
367
|
return 0
|
|
354
368
|
fi
|
|
355
369
|
|
|
@@ -372,17 +372,32 @@ resolve_openspec_capability_slug() {
|
|
|
372
372
|
sanitize_slug "$task_slug" "general-behavior"
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
resolve_repo_prefix() {
|
|
376
|
+
local root
|
|
377
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
378
|
+
basename "$root"
|
|
379
|
+
}
|
|
380
|
+
|
|
375
381
|
resolve_worktree_leaf() {
|
|
376
382
|
local branch_name="$1"
|
|
377
383
|
local masterplan_label=""
|
|
378
384
|
local branch_role=""
|
|
379
385
|
local branch_leaf=""
|
|
386
|
+
local repo_prefix
|
|
387
|
+
repo_prefix="$(resolve_repo_prefix)"
|
|
380
388
|
|
|
381
389
|
masterplan_label="$(resolve_openspec_masterplan_label)"
|
|
382
390
|
if [[ -n "$masterplan_label" ]] && [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
|
|
383
391
|
branch_role="${BASH_REMATCH[1]}"
|
|
384
392
|
branch_leaf="${BASH_REMATCH[2]}"
|
|
385
|
-
printf '
|
|
393
|
+
printf '%s__%s__%s__%s' "$repo_prefix" "$branch_role" "$masterplan_label" "$branch_leaf"
|
|
394
|
+
return 0
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
if [[ "$branch_name" =~ ^agent/([^/]+)/(.+)$ ]]; then
|
|
398
|
+
branch_role="${BASH_REMATCH[1]}"
|
|
399
|
+
branch_leaf="${BASH_REMATCH[2]}"
|
|
400
|
+
printf '%s__%s__%s' "$repo_prefix" "$branch_role" "$branch_leaf"
|
|
386
401
|
return 0
|
|
387
402
|
fi
|
|
388
403
|
|
|
@@ -56,6 +56,7 @@ const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [
|
|
|
56
56
|
const SESSION_ACTIVITY_GROUPS = [
|
|
57
57
|
{ kind: 'blocked', label: 'BLOCKED' },
|
|
58
58
|
{ kind: 'working', label: 'WORKING NOW' },
|
|
59
|
+
{ kind: 'finished', label: 'FINISHED' },
|
|
59
60
|
{ kind: 'idle', label: 'THINKING' },
|
|
60
61
|
{ kind: 'stalled', label: 'STALLED' },
|
|
61
62
|
{ kind: 'dead', label: 'DEAD' },
|
|
@@ -63,6 +64,7 @@ const SESSION_ACTIVITY_GROUPS = [
|
|
|
63
64
|
const SESSION_ACTIVITY_ICON_IDS = {
|
|
64
65
|
blocked: 'warning',
|
|
65
66
|
working: 'loading~spin',
|
|
67
|
+
finished: 'pass-filled',
|
|
66
68
|
idle: 'comment-discussion',
|
|
67
69
|
stalled: 'clock',
|
|
68
70
|
dead: 'error',
|
|
@@ -108,6 +110,10 @@ function iconColorId(iconId) {
|
|
|
108
110
|
return 'terminal.ansiCyan';
|
|
109
111
|
case 'list-tree':
|
|
110
112
|
return 'terminal.ansiBlue';
|
|
113
|
+
case 'pass-filled':
|
|
114
|
+
case 'pass':
|
|
115
|
+
case 'check':
|
|
116
|
+
return 'testing.iconPassed';
|
|
111
117
|
default:
|
|
112
118
|
return '';
|
|
113
119
|
}
|
|
@@ -468,9 +474,15 @@ function agentBadgeFromBranch(branch) {
|
|
|
468
474
|
|
|
469
475
|
function buildActiveAgentsStatusSummary(summary) {
|
|
470
476
|
const workingCount = summary?.workingCount || 0;
|
|
477
|
+
const finishedCount = summary?.finishedCount || 0;
|
|
471
478
|
const idleCount = summary?.idleCount || 0;
|
|
472
|
-
if (workingCount > 0 || idleCount > 0) {
|
|
473
|
-
|
|
479
|
+
if (workingCount > 0 || finishedCount > 0 || idleCount > 0) {
|
|
480
|
+
const parts = [`${workingCount} working`];
|
|
481
|
+
if (finishedCount > 0) {
|
|
482
|
+
parts.push(`${finishedCount} finished`);
|
|
483
|
+
}
|
|
484
|
+
parts.push(`${idleCount} idle`);
|
|
485
|
+
return `$(git-branch) ${parts.join(' · ')}`;
|
|
474
486
|
}
|
|
475
487
|
return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`;
|
|
476
488
|
}
|
|
@@ -490,6 +502,7 @@ function buildActiveAgentsStatusTooltip(selectedSession, summary) {
|
|
|
490
502
|
return [
|
|
491
503
|
formatCountLabel(activeCount, 'active agent'),
|
|
492
504
|
formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'),
|
|
505
|
+
formatCountLabel(summary?.finishedCount || 0, 'finished session'),
|
|
493
506
|
formatCountLabel(summary?.idleCount || 0, 'idle session'),
|
|
494
507
|
formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
|
|
495
508
|
formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
|
|
@@ -534,6 +547,10 @@ function countWorkingSessions(sessions) {
|
|
|
534
547
|
)).length;
|
|
535
548
|
}
|
|
536
549
|
|
|
550
|
+
function countFinishedSessions(sessions) {
|
|
551
|
+
return sessions.filter((session) => session.activityKind === 'finished').length;
|
|
552
|
+
}
|
|
553
|
+
|
|
537
554
|
function countIdleSessions(sessions) {
|
|
538
555
|
return sessions.filter((session) => (
|
|
539
556
|
session.activityKind === 'idle' || session.activityKind === 'stalled'
|
|
@@ -571,6 +588,9 @@ function sessionFreshnessLabel(session, now = Date.now()) {
|
|
|
571
588
|
if (session.activityKind === 'blocked') {
|
|
572
589
|
return 'Needs attention';
|
|
573
590
|
}
|
|
591
|
+
if (session.activityKind === 'finished') {
|
|
592
|
+
return 'Finished';
|
|
593
|
+
}
|
|
574
594
|
if (session.activityKind === 'stalled') {
|
|
575
595
|
return 'Possibly stale';
|
|
576
596
|
}
|
|
@@ -598,6 +618,8 @@ function sessionStatusLabel(session) {
|
|
|
598
618
|
return 'Blocked';
|
|
599
619
|
case 'working':
|
|
600
620
|
return 'Working';
|
|
621
|
+
case 'finished':
|
|
622
|
+
return 'Finished';
|
|
601
623
|
case 'idle':
|
|
602
624
|
return 'Idle';
|
|
603
625
|
case 'stalled':
|
|
@@ -804,6 +826,7 @@ function buildWorktreeBranchDescription(sessions) {
|
|
|
804
826
|
function buildOverviewDescription(summary) {
|
|
805
827
|
return [
|
|
806
828
|
formatCountLabel(summary?.workingCount || 0, 'working agent'),
|
|
829
|
+
formatCountLabel(summary?.finishedCount || 0, 'finished agent'),
|
|
807
830
|
formatCountLabel(summary?.idleCount || 0, 'idle agent'),
|
|
808
831
|
formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
|
|
809
832
|
formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
|
|
@@ -923,6 +946,9 @@ function workingSessionSortKey(session) {
|
|
|
923
946
|
if (session.deltaLabel === 'New') {
|
|
924
947
|
return 3;
|
|
925
948
|
}
|
|
949
|
+
if (session.activityKind === 'finished') {
|
|
950
|
+
return 5;
|
|
951
|
+
}
|
|
926
952
|
return 4;
|
|
927
953
|
}
|
|
928
954
|
|
|
@@ -2490,6 +2516,7 @@ function buildRepoOverview(sessions, unassignedChanges, lockEntries) {
|
|
|
2490
2516
|
return {
|
|
2491
2517
|
sessionCount: sessions.length,
|
|
2492
2518
|
workingCount: countWorkingSessions(sessions),
|
|
2519
|
+
finishedCount: countFinishedSessions(sessions),
|
|
2493
2520
|
idleCount: countIdleSessions(sessions),
|
|
2494
2521
|
unassignedChangeCount: (unassignedChanges || []).length,
|
|
2495
2522
|
lockedFileCount: Array.isArray(lockEntries) ? lockEntries.length : 0,
|
|
@@ -2860,6 +2887,7 @@ class ActiveAgentsProvider {
|
|
|
2860
2887
|
this.viewSummary = {
|
|
2861
2888
|
sessionCount: 0,
|
|
2862
2889
|
workingCount: 0,
|
|
2890
|
+
finishedCount: 0,
|
|
2863
2891
|
idleCount: 0,
|
|
2864
2892
|
unassignedChangeCount: 0,
|
|
2865
2893
|
lockedFileCount: 0,
|
|
@@ -2878,6 +2906,7 @@ class ActiveAgentsProvider {
|
|
|
2878
2906
|
this.updateViewState({
|
|
2879
2907
|
sessionCount: 0,
|
|
2880
2908
|
workingCount: 0,
|
|
2909
|
+
finishedCount: 0,
|
|
2881
2910
|
idleCount: 0,
|
|
2882
2911
|
unassignedChangeCount: 0,
|
|
2883
2912
|
lockedFileCount: 0,
|
|
@@ -3000,6 +3029,10 @@ class ActiveAgentsProvider {
|
|
|
3000
3029
|
const summary = {
|
|
3001
3030
|
sessionCount: repoEntries.reduce((total, entry) => total + entry.sessions.length, 0),
|
|
3002
3031
|
workingCount: repoEntries.reduce((total, entry) => total + entry.overview.workingCount, 0),
|
|
3032
|
+
finishedCount: repoEntries.reduce(
|
|
3033
|
+
(total, entry) => total + (entry.overview.finishedCount || 0),
|
|
3034
|
+
0,
|
|
3035
|
+
),
|
|
3003
3036
|
idleCount: repoEntries.reduce((total, entry) => total + entry.overview.idleCount, 0),
|
|
3004
3037
|
unassignedChangeCount: repoEntries.reduce(
|
|
3005
3038
|
(total, entry) => total + entry.overview.unassignedChangeCount,
|
|
@@ -3049,6 +3082,7 @@ class ActiveAgentsProvider {
|
|
|
3049
3082
|
}),
|
|
3050
3083
|
], {
|
|
3051
3084
|
description: '1',
|
|
3085
|
+
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3052
3086
|
}),
|
|
3053
3087
|
];
|
|
3054
3088
|
|
|
@@ -3056,6 +3090,7 @@ class ActiveAgentsProvider {
|
|
|
3056
3090
|
if (workingNowItems.length > 0) {
|
|
3057
3091
|
sectionItems.push(new SectionItem('Working now', workingNowItems, {
|
|
3058
3092
|
description: String(workingNowItems.length),
|
|
3093
|
+
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3059
3094
|
iconId: 'loading~spin',
|
|
3060
3095
|
}));
|
|
3061
3096
|
}
|
|
@@ -3096,7 +3131,7 @@ class ActiveAgentsProvider {
|
|
|
3096
3131
|
if (advancedItems.length > 0) {
|
|
3097
3132
|
sectionItems.push(new SectionItem('Advanced details', advancedItems, {
|
|
3098
3133
|
description: String(advancedItems.length),
|
|
3099
|
-
collapsedState: vscode.TreeItemCollapsibleState.
|
|
3134
|
+
collapsedState: vscode.TreeItemCollapsibleState.Expanded,
|
|
3100
3135
|
iconId: 'list-tree',
|
|
3101
3136
|
}));
|
|
3102
3137
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "GitGuardex Active Agents",
|
|
4
4
|
"description": "Shows live Guardex sandbox sessions and repo changes in a dedicated VS Code Active Agents sidebar.",
|
|
5
5
|
"publisher": "recodeee",
|
|
6
|
-
"version": "0.0.
|
|
6
|
+
"version": "0.0.19",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"icon": "icon.png",
|
|
9
9
|
"engines": {
|
|
@@ -700,17 +700,35 @@ function deriveSessionActivity(session, options = {}) {
|
|
|
700
700
|
.filter(Boolean))]
|
|
701
701
|
.sort((left, right) => left.localeCompare(right));
|
|
702
702
|
|
|
703
|
+
const workingLatestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, {
|
|
704
|
+
now,
|
|
705
|
+
useCache: options.useCache,
|
|
706
|
+
});
|
|
707
|
+
const workingLastFileActivityAt = Number.isFinite(workingLatestFileActivityMs)
|
|
708
|
+
? new Date(workingLatestFileActivityMs).toISOString()
|
|
709
|
+
: '';
|
|
710
|
+
const workingLastFileActivityLabel = workingLastFileActivityAt
|
|
711
|
+
? formatElapsedFrom(workingLastFileActivityAt, now)
|
|
712
|
+
: '';
|
|
713
|
+
const workingFileActivityAgeMs = Number.isFinite(workingLatestFileActivityMs)
|
|
714
|
+
? Math.max(0, now - workingLatestFileActivityMs)
|
|
715
|
+
: null;
|
|
716
|
+
const isFinishedUncommitted = workingFileActivityAgeMs !== null
|
|
717
|
+
&& workingFileActivityAgeMs > IDLE_ACTIVITY_WINDOW_MS;
|
|
718
|
+
|
|
703
719
|
return {
|
|
704
|
-
activityKind: 'working',
|
|
705
|
-
activityLabel: 'working',
|
|
720
|
+
activityKind: isFinishedUncommitted ? 'finished' : 'working',
|
|
721
|
+
activityLabel: isFinishedUncommitted ? 'finished' : 'working',
|
|
706
722
|
activityCountLabel: formatFileCount(worktreeChangedPaths.length),
|
|
707
|
-
activitySummary:
|
|
723
|
+
activitySummary: isFinishedUncommitted && workingLastFileActivityLabel
|
|
724
|
+
? `${previewChangedPaths(worktreeChangedPaths)} · idle ${workingLastFileActivityLabel}`
|
|
725
|
+
: previewChangedPaths(worktreeChangedPaths),
|
|
708
726
|
changeCount: worktreeChangedPaths.length,
|
|
709
727
|
changedPaths,
|
|
710
728
|
worktreeChangedPaths: worktreeRelativePaths,
|
|
711
729
|
pidAlive,
|
|
712
|
-
lastFileActivityAt:
|
|
713
|
-
lastFileActivityLabel:
|
|
730
|
+
lastFileActivityAt: workingLastFileActivityAt,
|
|
731
|
+
lastFileActivityLabel: workingLastFileActivityLabel,
|
|
714
732
|
};
|
|
715
733
|
}
|
|
716
734
|
|