mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 02:34:15 -05:00
feat(report): generate deterministic session task reports (#1802)
* feat(report): generate deterministic session task reports * fix(report): address task report review findings * fix(report): stabilize task report paths on Windows * test(report): expect redacted git metadata cwd * test(report): assert literal redacted git cwd * fix(report): capture PowerShell and backgrounded validations * fix(report): detect quoted validation commands * fix(report): reconcile background validation notifications * fix(report): keep foreground command statuses authoritative * test(report): assert command status precedence
This commit is contained in:
@@ -377,6 +377,9 @@ openclaude doctor report --markdown
|
||||
# write a redacted JSON issue report for attachment
|
||||
openclaude doctor report --json --out openclaude-report.json
|
||||
|
||||
# write a deterministic JSON task report from a session transcript
|
||||
openclaude report --json --transcript ~/.openclaude/projects/-path-to-project/session-id.jsonl --out task-report.json
|
||||
|
||||
# full local hardening check (smoke + runtime doctor)
|
||||
bun run hardening:check
|
||||
|
||||
@@ -391,6 +394,7 @@ Notes:
|
||||
- Local providers such as `http://localhost:11434/v1`, `http://10.0.0.1:11434/v1`, and `http://127.0.0.1:1337/v1` can run without `OPENAI_API_KEY`.
|
||||
- Codex profiles validate `CODEX_API_KEY` or the Codex CLI auth file and probe `POST /responses` instead of `GET /models`.
|
||||
- `openclaude doctor report` is redacted by default and is intended for GitHub issues. It summarizes provider/runtime/build/settings state without prompts, transcripts, raw settings files, API keys, MCP command details, or full home-directory paths.
|
||||
- `openclaude report --json` summarizes observed session facts such as tool uses, Bash commands, validation commands, changed files, branch metadata, warnings, and linked issue/PR references. Use `--transcript <file>` for an explicit transcript, `--session <id>` for a stored session, or omit both to report the latest session for the current project. Large previews are truncated and credential-shaped strings are redacted. When no validation command is observed, the report keeps `validations` empty and includes a warning instead of claiming checks passed.
|
||||
|
||||
## Provider Launch Profiles
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import {
|
||||
buildTaskReport,
|
||||
formatTaskReportAsJson,
|
||||
writeTaskReport,
|
||||
type TaskReportArgs,
|
||||
} from '../../utils/taskReport.js'
|
||||
import {
|
||||
getProjectDir,
|
||||
getSessionFilesWithMtime,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
function writeLine(
|
||||
stream: NodeJS.WritableStream,
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.write(`${message}\n`, error => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function taskReportHandler(
|
||||
options: TaskReportArgs,
|
||||
): Promise<void> {
|
||||
if (options.format !== 'json') {
|
||||
throw new Error('Task reports currently support JSON output only')
|
||||
}
|
||||
|
||||
const cwd = resolve(options.cwd ?? process.cwd())
|
||||
const transcriptPath = await resolveTranscriptPath({
|
||||
cwd,
|
||||
sessionId: options.sessionId ?? null,
|
||||
transcriptPath: options.transcriptPath ?? null,
|
||||
})
|
||||
const report = await buildTaskReport({
|
||||
transcriptPath,
|
||||
cwd,
|
||||
})
|
||||
const content = formatTaskReportAsJson(report)
|
||||
|
||||
if (options.outFile) {
|
||||
const outputPath = await writeTaskReport(options.outFile, content)
|
||||
await writeLine(process.stderr, `Task report written to ${outputPath}`)
|
||||
return
|
||||
}
|
||||
|
||||
await writeLine(process.stdout, content)
|
||||
}
|
||||
|
||||
export async function printTaskReportError(error: unknown): Promise<void> {
|
||||
await writeLine(
|
||||
process.stderr,
|
||||
`Failed to generate task report: ${formatError(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
async function resolveTranscriptPath({
|
||||
cwd,
|
||||
sessionId,
|
||||
transcriptPath,
|
||||
}: {
|
||||
cwd: string
|
||||
sessionId: string | null
|
||||
transcriptPath: string | null
|
||||
}): Promise<string> {
|
||||
if (transcriptPath) {
|
||||
return resolve(cwd, transcriptPath)
|
||||
}
|
||||
|
||||
const sessionFiles = await getSessionFilesWithMtime(getProjectDir(cwd))
|
||||
if (sessionId) {
|
||||
const sessionFile = sessionFiles.get(sessionId)
|
||||
if (!sessionFile) {
|
||||
throw new Error(`Session transcript not found: ${sessionId}`)
|
||||
}
|
||||
return sessionFile.path
|
||||
}
|
||||
|
||||
let latest: { path: string; mtime: number; ctime: number } | null = null
|
||||
for (const sessionFile of sessionFiles.values()) {
|
||||
if (
|
||||
!latest ||
|
||||
sessionFile.mtime > latest.mtime ||
|
||||
(sessionFile.mtime === latest.mtime &&
|
||||
(sessionFile.ctime > latest.ctime ||
|
||||
(sessionFile.ctime === latest.ctime &&
|
||||
sessionFile.path.localeCompare(latest.path) < 0)))
|
||||
) {
|
||||
latest = sessionFile
|
||||
}
|
||||
}
|
||||
if (!latest) {
|
||||
throw new Error(
|
||||
'No session transcripts found for the current project. Pass --transcript <file> or --session <id>.',
|
||||
)
|
||||
}
|
||||
return latest.path
|
||||
}
|
||||
@@ -4215,6 +4215,53 @@ async function run(): Promise<CommanderCommand> {
|
||||
});
|
||||
}
|
||||
|
||||
program
|
||||
.command('report')
|
||||
.description(
|
||||
'Generate a deterministic JSON task report for an OpenClaude session',
|
||||
)
|
||||
.option('--json', 'Print JSON output')
|
||||
.option('--transcript <file>', 'Path to a session JSONL transcript')
|
||||
.option(
|
||||
'--session <id>',
|
||||
'Session ID to report (defaults to latest session in the current project)',
|
||||
)
|
||||
.option('--out <file>', 'Write the report to a file')
|
||||
.action(async (options: {
|
||||
json?: boolean;
|
||||
transcript?: string;
|
||||
session?: string;
|
||||
out?: string;
|
||||
}) => {
|
||||
const {
|
||||
taskReportHandler,
|
||||
printTaskReportError,
|
||||
} = await import('./cli/handlers/taskReport.js');
|
||||
try {
|
||||
if (options.json !== true) {
|
||||
throw new Error(
|
||||
'Task reports currently support JSON output only. Pass --json.',
|
||||
);
|
||||
}
|
||||
if (options.transcript && options.session) {
|
||||
throw new Error(
|
||||
'Pass either --transcript <file> or --session <id>, not both.',
|
||||
);
|
||||
}
|
||||
await taskReportHandler({
|
||||
format: 'json',
|
||||
transcriptPath: options.transcript ?? null,
|
||||
sessionId: options.session ?? null,
|
||||
outFile: options.out ?? null,
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
await printTaskReportError(error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Doctor command - check installation health
|
||||
const doctorCommand = program
|
||||
.command('doctor')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user