Skip to content

Commit 3ed0abc

Browse files
authored
feat(cli): simplify identity diagnostics (#124)
* feat(cli): simplify identity diagnostics Make status and doctor concise, with --verbose for full diagnostic detail. Keep actual push destinations, distinct warnings and one actionable next step. Preserve short/JSON contracts, exits and offline behavior; add no prompts or dependencies. Verified build, docs, 358 unit, 54 E2E and 138 release-workflow tests; 252 captured observations cover machine compatibility and effect boundaries. Two Fish tests and one optional installation test skipped. Live authentication and native screen-reader qualification were not performed. * test(auth): wait for descendant termination before heartbeat checks Use a bounded, identity-checked observation helper before sampling the heartbeat. Keep the overall cleanup deadline and first-signal exits. Add deterministic ordering and survivor controls, plus synthetic TAP diagnostics for termination, timing and heartbeat observations. No production runtime, public UX/schema, version or release changes. Validation: build, unit (360 pass, 2 environment skips), E2E (54 pass, 1 optional install skip), release (138 pass), docs, auth (27 pass), help/version and diff checks passed. Independent mutation controls rejected missing observation and exceeded cleanup bounds.
1 parent 95c0a71 commit 3ed0abc

18 files changed

Lines changed: 517 additions & 127 deletions

‎README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ Use your actual username. `add` replaces the profile; include any other fields y
7070

7171
For SSH prerequisites and a full walkthrough, read [Use the right Git identity for this repo](https://docs.gitrole.dev/guides/use-the-right-git-identity-for-this-repo/).
7272

73+
Human `status` and `doctor` output is concise. Each diagnostic warning appears
74+
once; doctor lists every push destination, counts ok checks and keeps warn/info.
75+
Its `next` action is the same in both modes and never runs automatically. Use
76+
`gitrole status --verbose` or `gitrole doctor --verbose` for full explanations.
77+
Add `--offline` to keep diagnosis local. Verbosity does not add checks or change
78+
verdicts. `status --short` and `doctor --json` retain their exact machine output
79+
and exit behavior, including when combined with `--verbose`.
80+
7381
## Interpret results
7482

7583
| Command | Purpose |

‎completions/_gitrole‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ case $state in
146146
status)
147147
_arguments \
148148
'--short[show machine-friendly one-line status output]' \
149+
'--verbose[show full human-readable diagnostic details]' \
149150
'--offline[skip the live SSH githubUser probe and use local checks only]' \
150151
'(- :)'{-h,--help}'[print help]' \
151152
&& ret=0
@@ -154,6 +155,7 @@ case $state in
154155
_arguments \
155156
'--json[write the diagnostic result as JSON]' \
156157
'--offline[skip all SSH execution and use local checks only]' \
158+
'--verbose[show full human-readable diagnostic details]' \
157159
'(- :)'{-h,--help}'[print help]' \
158160
&& ret=0
159161
;;

‎completions/gitrole.bash‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,12 @@ _gitrole() {
188188
;;
189189
status)
190190
if [[ "$cur" == -* ]]; then
191-
_gitrole_comp_words "--short --offline --help -h"
191+
_gitrole_comp_words "--short --offline --verbose --help -h"
192192
fi
193193
;;
194194
doctor)
195195
if [[ "$cur" == -* ]]; then
196-
_gitrole_comp_words "--json --offline --help -h"
196+
_gitrole_comp_words "--json --offline --verbose --help -h"
197197
fi
198198
;;
199199
current|list)

‎completions/gitrole.fish‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,5 @@ complete -c gitrole -f -n '__fish_seen_subcommand_from help' -a 'add import use
5858

5959
complete -c gitrole -f -n '__fish_use_subcommand' -a auth -d 'Explicit authentication checks'
6060
complete -c gitrole -f -n '__fish_seen_subcommand_from auth; and not __fish_seen_subcommand_from test' -a test -d 'Observe SSH accounts'
61+
62+
complete -c gitrole -f -n '__fish_seen_subcommand_from status doctor' -l verbose -d 'Show full human-readable diagnostic details'

‎docs/commands.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,39 @@ Key loading passes `--` before the path. This follows OpenSSH argument parsing
123123
`GITROLE_SSH_ADD_BIN` executables must accept the same argument convention;
124124
arbitrary wrappers are not qualified. This does not guarantee that a key exists
125125
or that loading it will succeed.
126+
127+
### Human diagnostic detail
128+
129+
`gitrole status` shows the role, effective identity, scope and check outcomes.
130+
`gitrole doctor` lists every push destination in both modes, counts successful
131+
checks, and shows each warning and informational check. Verbose also lists every
132+
successful check; counts describe checks, not verified accounts or destinations.
133+
Multiline explanations stay indented beneath their check.
134+
135+
Both modes give the same next action, ordered by missing/unobservable destination,
136+
history, identity/policy, then auth; remaining host/transport warnings follow.
137+
History advice requires resolving identity/policy warnings before making a commit.
138+
Outside a repository, enter the intended repository first. Policy conflicts ask
139+
you to review the effective identity and `.gitrole` allowed/default roles.
140+
For supported SSH destinations, auth advice offers the existing deliberate
141+
`gitrole auth test`, with its network/command/state effects and future-push limits.
142+
Unsupported transport warnings never imply an equivalent plain SSH command.
143+
The suggested action is never executed automatically. Unknown diagnostic errors
144+
keep their original detail rather than being guessed into an authentication cause.
145+
146+
Use `gitrole status --verbose` or `gitrole doctor --verbose` for full reasons.
147+
Doctor also expands repository and configuration context. `--verbose` changes
148+
presentation only: no extra Git/SSH checks, writes or interactive prompts.
149+
Combine it with `--offline` to keep the existing zero-SSH boundary.
150+
151+
Machine modes take precedence: `status --short --verbose` is identical to
152+
`status --short`; `doctor --json --verbose` is identical to `doctor --json`.
153+
Exits and report streams are unchanged. Human reports (including check warnings)
154+
are on stdout; operational errors remain on stderr. Use machine modes for scripts.
155+
156+
A missing destination remains unverified; configure your intended push destination
157+
before retrying. `remote set` rewrites an existing origin, not creates one. An
158+
expected account is not an observed account. Offline/skipped and HTTPS-not-applicable
159+
authentication do not verify an account or a future push. In an unborn repository,
160+
status may exit 0 while doctor exits 2 for its history warning; verbosity preserves
161+
that distinction. Guided setup and a full-screen TUI are not part of this change.

‎docs/guides/troubleshoot-identity-warnings.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,20 @@ Check `gitrole --version` and `command -v gitrole`, then read <a href="{{ '/guid
7474
<h2 id="limits">An aligned result still cannot establish push success</h2>
7575

7676
A check does not prove refspec readiness or repository authorization. It observes current identity and default destinations. A later explicit `git push` target, `git commit --author`, or changed environment/configuration falls outside that snapshot.
77+
78+
## Read concise output and expand details
79+
80+
Start with `gitrole status` for check outcomes, or `gitrole doctor` for the
81+
warnings and every push destination. Default doctor counts ok checks; verbose
82+
lists them. Warn/info checks remain visible in both modes. Each warning is shown once. The `next` line is advice, not an
83+
automatic repair. Use `gitrole doctor --verbose` for complete reasons and context;
84+
use `gitrole doctor --offline --verbose` when SSH execution is not appropriate.
85+
86+
With no default push destination, choose and configure the intended destination
87+
before retrying; gitrole won't guess the URL. Next-action priority is destination,
88+
then history, identity/policy, then auth, and does not change with verbosity.
89+
Resolve identity/policy warnings before making the first commit. A matching local identity can
90+
coexist with unverified authentication. Do not change SSH settings merely to clear
91+
a warning. An explicit `gitrole auth test` remains a separate deliberate action;
92+
it may prompt, run configured commands or change SSH state, and its connection
93+
observation does not clear doctor's verdict or prove a future push.

‎src/application/alignment.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,11 @@ export function buildPushAlignmentChecks(input: {
329329
}
330330

331331
/** Condenses a probe-owned reason message for human output, preserving the full message for JSON. */
332-
export function summarizeSshMessage(message: string): string {
332+
export function summarizeSshMessage(message: string, verbose = false): string {
333+
if (verbose) return message;
333334
const match = /^SSH account unverified:\n((?:- [^\n]+\n)+)\n([\s\S]*)$/.exec(message);
334335
if (!match) return message;
335336
const reasons = match[1].trimEnd().split('\n').map((line) => line.slice(2));
336-
const more = reasons.length > 1 ? ' (and other differences; see gitrole doctor --json).' : '';
337-
return `SSH account unverified: ${reasons[0]}${more} ${match[2]}`;
337+
const more = reasons.length > 1 ? ` (and ${reasons.length - 1} other differences)` : '';
338+
return `SSH account unverified: ${reasons[0]}${more}`;
338339
}

‎src/application/use-cases/status.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { collectObservedState, type ObservedState } from '../observed-state.js';
1515
import { evaluateRepoPolicy, loadOptionalRepoPolicy } from '../repo-policy.js';
1616

1717
export interface StatusOptions {
18+
/** Include full human explanations without changing observations or verdicts. */
19+
verbose?: boolean;
1820
/**
1921
* Skip the live SSH githubUser probe.
2022
* SSH `auth` is `na`. HTTPS pin checks still use the saved role and `.gitrole`.
@@ -48,7 +50,7 @@ export async function getStatus(
4850
return {
4951
roleName: role?.name ?? UNMATCHED_ROLE_NAME,
5052
commitIdentity,
51-
pushAuth: formatPushAuth(role, observedState, httpsAuth, offline),
53+
pushAuth: formatPushAuth(role, observedState, httpsAuth, offline, options.verbose === true),
5254
scope: observedState.scope.effective,
5355
localOverride: observedState.scope.hasLocalOverride,
5456
lastNonMergeCommit,
@@ -106,15 +108,16 @@ function formatPushAuth(
106108
sshAuth?: DoctorResult['sshAuth'];
107109
},
108110
httpsAuth?: HttpsAuthDescription,
109-
offline = false
111+
offline = false,
112+
verbose = false
110113
): string | undefined {
111114
if (observedState.repository.push) {
112115
const push = observedState.repository.push;
113116
if (push.targets.length === 1 && push.targets[0].remote.protocol === 'https' && httpsAuth) return formatHttpsPushAuth(httpsAuth);
114117
if (push.message || !push.targets.length) return push.message ?? 'push destination unverified';
115118
return push.targets.map(({ remote, sshAuth, message }) =>
116119
sshAuth?.ok && sshAuth.githubUser ? `${sshAuth.githubUser} via ${remote.host}` :
117-
`${remote.name} ${remote.url} (${offline && remote.protocol === 'ssh' ? 'SSH authentication skipped (--offline); local checks only. An online check may connect and run configured SSH commands or change SSH state.' : message ?? (sshAuth?.message ? summarizeSshMessage(sshAuth.message) : 'authentication unverified')})`
120+
`${remote.name} ${remote.url} (${offline && remote.protocol === 'ssh' ? 'SSH authentication skipped (--offline); local checks only. An online check may connect and run configured SSH commands or change SSH state.' : message ?? (sshAuth?.message ? summarizeSshMessage(sshAuth.message, verbose) : 'authentication unverified')})`
118121
).join('; ');
119122
}
120123
if (observedState.sshAuth?.githubUser) {

‎src/cli/index.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ Examples:
342342
.command('status')
343343
.description('check whether the current repo is aligned for commit and push')
344344
.option('--short', 'show machine-friendly one-line status output')
345+
.option('--verbose', 'show full human-readable diagnostic details')
345346
.option('--offline', 'skip the live SSH githubUser probe and use local checks only')
346347
.addHelpText(
347348
'after',
@@ -362,6 +363,8 @@ HTTPS auth is unchanged, because it uses the saved role and .gitrole, not the ne
362363
363364
Use this when you want to know whether the repo looks ready to commit or push.
364365
Use 'gitrole current' when you only want to know which saved role matches the active commit identity.
366+
Human output is concise; use --verbose for full explanations. --short takes
367+
precedence over --verbose and keeps its exact machine contract.
365368
Use 'gitrole status --short --offline' for a shell prompt. It does not open SSH.
366369
367370
Policy:
@@ -391,9 +394,9 @@ Shell prompt:
391394
It is opt-in. It does not switch roles.
392395
`
393396
)
394-
.action(async (options: { short?: boolean; offline?: boolean }) => {
395-
const result = await getStatus(dependencies, { offline: options.offline === true });
396-
io.stdout(options.short ? renderShortStatus(result) : renderStatus(result));
397+
.action(async (options: { short?: boolean; offline?: boolean; verbose?: boolean }) => {
398+
const result = await getStatus(dependencies, { offline: options.offline === true, verbose: !options.short && options.verbose === true });
399+
io.stdout(options.short ? renderShortStatus(result) : renderStatus(result, options));
397400
commandExitCode = result.overall === 'aligned' ? 0 : 2;
398401
});
399402

@@ -418,12 +421,18 @@ Failures write reasons to stderr. This command never changes roles or Git config
418421
program
419422
.command('doctor')
420423
.description('diagnose identity, remote, and SSH auth alignment')
424+
.option('--verbose', 'show full human-readable diagnostic details')
421425
.option('--json', 'write the diagnostic result as JSON')
422426
.option('--offline', 'skip all SSH execution and use local checks only')
423427
.addHelpText(
424428
'after',
425429
`
426430
431+
Human output lists every push destination, counts ok checks and keeps warn/info
432+
checks. --verbose expands all checks and context without additional checks.
433+
Both modes give the same next action: destination, history, identity/policy, then auth. --json takes precedence over
434+
--verbose and keeps the same full result and exit code.
435+
427436
Checks:
428437
- effective git identity
429438
- repository context and branch
@@ -455,9 +464,9 @@ Offline:
455464
configured commands, contact the network or change SSH state.
456465
`
457466
)
458-
.action(async (options: { json?: boolean; offline?: boolean }) => {
467+
.action(async (options: { json?: boolean; offline?: boolean; verbose?: boolean }) => {
459468
const result = await doctor(dependencies, { offline: options.offline === true });
460-
io.stdout(options.json ? JSON.stringify(result, null, 2) : renderDoctor(result));
469+
io.stdout(options.json ? JSON.stringify(result, null, 2) : renderDoctor(result, 'doctor', options));
461470
commandExitCode = getDoctorExitCode(result);
462471
});
463472

0 commit comments

Comments
 (0)