@jameslovespancakes/pi-plus 1.0.10 → 1.0.12

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.
Files changed (120) hide show
  1. package/README.md +44 -20
  2. package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
  3. package/package.json +6 -3
  4. package/src/core/accounts/oauth-pool.ts +102 -0
  5. package/src/core/accounts/registry.ts +8 -25
  6. package/src/core/accounts/routing.ts +122 -0
  7. package/src/core/anthropic/client-identity.ts +9 -64
  8. package/src/core/anthropic/quota.ts +8 -53
  9. package/src/core/anthropic/routing.ts +40 -115
  10. package/src/core/anthropic/store.ts +7 -9
  11. package/src/core/catalog/quality.ts +33 -15
  12. package/src/core/codex/quota.ts +8 -2
  13. package/src/core/codex/store.ts +12 -9
  14. package/src/core/config.ts +4 -17
  15. package/src/core/store.ts +13 -12
  16. package/src/domains/models/provider-picker.ts +1 -1
  17. package/src/domains/setup/index.ts +23 -23
  18. package/src/domains/subscriptions/accounts-picker.ts +18 -37
  19. package/src/domains/subscriptions/accounts.ts +23 -29
  20. package/src/domains/subscriptions/footer.ts +48 -29
  21. package/src/domains/subscriptions/index.ts +11 -27
  22. package/src/domains/subscriptions/provider.ts +38 -103
  23. package/src/domains/subscriptions/providers/anthropic.ts +5 -17
  24. package/src/domains/subscriptions/providers/codex.ts +171 -4
  25. package/src/domains/subscriptions/providers/hosted.ts +18 -0
  26. package/src/domains/subscriptions/providers/oauth-pool.ts +273 -0
  27. package/src/domains/subscriptions/routing.ts +19 -11
  28. package/src/domains/workflows/LICENSE.md +21 -0
  29. package/src/domains/workflows/index.ts +836 -0
  30. package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
  31. package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
  32. package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
  33. package/src/domains/workflows/runtime/agent-attempt.ts +147 -0
  34. package/src/domains/workflows/runtime/agent-limits.ts +43 -0
  35. package/src/domains/workflows/runtime/agent-replay.ts +399 -0
  36. package/src/domains/workflows/runtime/agent-retry.ts +116 -0
  37. package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
  38. package/src/domains/workflows/runtime/agent-runner.ts +203 -0
  39. package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
  40. package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
  41. package/src/domains/workflows/runtime/agent-session.ts +382 -0
  42. package/src/domains/workflows/runtime/agent-skills.ts +270 -0
  43. package/src/domains/workflows/runtime/agent-workspace.ts +103 -0
  44. package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
  45. package/src/domains/workflows/runtime/background-workflows.ts +492 -0
  46. package/src/domains/workflows/runtime/budget.ts +53 -0
  47. package/src/domains/workflows/runtime/cancellation.ts +87 -0
  48. package/src/domains/workflows/runtime/command-completions.ts +36 -0
  49. package/src/domains/workflows/runtime/concurrency.ts +403 -0
  50. package/src/domains/workflows/runtime/debug.ts +3 -0
  51. package/src/domains/workflows/runtime/diff-capture.ts +81 -0
  52. package/src/domains/workflows/runtime/discovery.ts +137 -0
  53. package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
  54. package/src/domains/workflows/runtime/dynamax.ts +330 -0
  55. package/src/domains/workflows/runtime/engine.ts +543 -0
  56. package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
  57. package/src/domains/workflows/runtime/finalizers.ts +66 -0
  58. package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
  59. package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
  60. package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
  61. package/src/domains/workflows/runtime/journal.ts +313 -0
  62. package/src/domains/workflows/runtime/model-profiles.ts +310 -0
  63. package/src/domains/workflows/runtime/options.ts +157 -0
  64. package/src/domains/workflows/runtime/perf.ts +146 -0
  65. package/src/domains/workflows/runtime/pi-compat.ts +32 -0
  66. package/src/domains/workflows/runtime/process-runner.ts +260 -0
  67. package/src/domains/workflows/runtime/progress-types.ts +59 -0
  68. package/src/domains/workflows/runtime/progress.ts +400 -0
  69. package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
  70. package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
  71. package/src/domains/workflows/runtime/research-contract.ts +89 -0
  72. package/src/domains/workflows/runtime/research-evidence.ts +289 -0
  73. package/src/domains/workflows/runtime/resume-context.ts +751 -0
  74. package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
  75. package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
  76. package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
  77. package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
  78. package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
  79. package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
  80. package/src/domains/workflows/runtime/review/review-format.ts +133 -0
  81. package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
  82. package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
  83. package/src/domains/workflows/runtime/review/review-report.ts +42 -0
  84. package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
  85. package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
  86. package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
  87. package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
  88. package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
  89. package/src/domains/workflows/runtime/session-identity.ts +6 -0
  90. package/src/domains/workflows/runtime/structured-output.ts +32 -0
  91. package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
  92. package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
  93. package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
  94. package/src/domains/workflows/runtime/types.ts +255 -0
  95. package/src/domains/workflows/runtime/ui/display-text.ts +13 -0
  96. package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
  97. package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
  98. package/src/domains/workflows/runtime/ui/workflow-inspector.ts +398 -0
  99. package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
  100. package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
  101. package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
  102. package/src/domains/workflows/runtime/unknown-error.ts +8 -0
  103. package/src/domains/workflows/runtime/usage.ts +341 -0
  104. package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
  105. package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
  106. package/src/domains/workflows/runtime/workflow-module.ts +75 -0
  107. package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
  108. package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
  109. package/src/domains/workflows/runtime/workflow-run-history.ts +162 -0
  110. package/src/domains/workflows/runtime/workflow-run-record.ts +635 -0
  111. package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
  112. package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
  113. package/src/domains/workflows/runtime/workflows.ts +40 -0
  114. package/src/domains/workflows/runtime/worktree.ts +581 -0
  115. package/src/domains/workflows/workflows/code-review.ts +232 -0
  116. package/src/domains/workflows/workflows/diagnose.ts +154 -0
  117. package/src/domains/workflows/workflows/perf-review.ts +149 -0
  118. package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
  119. package/src/domains/workflows/workflows/research.ts +169 -0
  120. package/src/services/usage-service.ts +6 -30
@@ -0,0 +1,153 @@
1
+ import { initialPatchValidation, PatchEvaluationSchema, validateCandidatePatch, type PatchValidation } from "./patch-validation.ts";
2
+ import { isFatalWorkflowError } from "../cancellation.ts";
3
+ import { unknownErrorMessage } from "../unknown-error.ts";
4
+ import type { ParallelSettledError } from "../concurrency.ts";
5
+ import type { IsolatedAgentResult, LoadedWorkflow, WorkflowApi, WorkflowModule } from "../types.ts";
6
+ import type { WorktreeBaseline } from "../worktree.ts";
7
+ import { loadWorkflow } from "../workflow-module.ts";
8
+ import { serializeReviewIssue, type ReviewIssue } from "./review-issues.ts";
9
+ import type { ReviewContext } from "./review-report.ts";
10
+ import { serializeReviewContext } from "./review-report.ts";
11
+
12
+ const REVIEW_FIX_PHASE = "Generate patch previews";
13
+ const REVIEW_FIX_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
14
+
15
+ export interface ReviewFixPreview {
16
+ readonly findingId: string;
17
+ readonly result: string;
18
+ readonly patch: string;
19
+ readonly changed: boolean;
20
+ readonly validation: PatchValidation;
21
+ }
22
+
23
+ export interface ReviewFixFailure {
24
+ readonly findingId: string;
25
+ readonly error: ParallelSettledError;
26
+ }
27
+
28
+ export type ReviewFixOutcome = ReviewFixPreview | ReviewFixFailure;
29
+
30
+ export interface ReviewFixWorkflowResult {
31
+ readonly summary: string;
32
+ readonly fixes: readonly ReviewFixOutcome[];
33
+ }
34
+
35
+ export type ReviewFixWorkflowApi = Pick<WorkflowApi, "agent" | "parallel" | "phase" | "cwd" | "signal">;
36
+
37
+ /** Build an ephemeral workflow that generates one isolated patch preview per finding. */
38
+ export function createReviewFixWorkflow(
39
+ issues: readonly ReviewIssue[],
40
+ context: ReviewContext | undefined,
41
+ baseline: WorktreeBaseline,
42
+ ): LoadedWorkflow {
43
+ const module: WorkflowModule = {
44
+ meta: {
45
+ name: "code-review-fix-previews",
46
+ description: "Generate isolated patch previews for selected code-review findings.",
47
+ phases: [{ title: REVIEW_FIX_PHASE }, { title: "Validate patch previews" }],
48
+ },
49
+ default: (api) => runReviewFixWorkflow(api, issues, context, baseline),
50
+ };
51
+ return loadWorkflow(
52
+ module,
53
+ {
54
+ kind: "unverifiable",
55
+ reason: "ephemeral review-fix workflows capture runtime findings and do not have immutable module provenance",
56
+ },
57
+ baseline,
58
+ );
59
+ }
60
+
61
+ export function buildFixAgentPrompt(issue: ReviewIssue, context: ReviewContext | undefined): string {
62
+ return `Generate a patch preview for exactly one verified code-review finding in your disposable git worktree.
63
+
64
+ Selected finding JSON:
65
+ \`\`\`json
66
+ ${JSON.stringify({ context: serializeReviewContext(context), issue: serializeReviewIssue(issue) })}
67
+ \`\`\`
68
+
69
+ Instructions:
70
+ - Inspect the finding and cited evidence before editing.
71
+ - Make the smallest edit that addresses only this finding.
72
+ - Preserve unrelated user changes and avoid broad refactors.
73
+ - Run focused validation if an appropriate local check is available.
74
+ - Do not create commits or branches; the engine captures your worktree diff automatically.
75
+ - Do not post GitHub PR comments or any upstream review comments.
76
+ - Finish with a concise summary of changed files and validation results; this text is returned alongside the captured patch.`;
77
+ }
78
+
79
+ export async function runReviewFixWorkflow(
80
+ api: ReviewFixWorkflowApi,
81
+ issues: readonly ReviewIssue[],
82
+ context: ReviewContext | undefined,
83
+ baseline: WorktreeBaseline,
84
+ ): Promise<ReviewFixWorkflowResult> {
85
+ api.phase(REVIEW_FIX_PHASE);
86
+ const settled = await api.parallel(
87
+ issues.map((issue) => async (): Promise<ReviewFixPreview> => {
88
+ const isolated = await api.agent(buildFixAgentPrompt(issue, context), {
89
+ isolation: "worktree",
90
+ label: `fix:${issue.id}`,
91
+ phase: REVIEW_FIX_PHASE,
92
+ profile: "medium",
93
+ cacheKey: `review-fix:${issue.id}`,
94
+ tools: [...REVIEW_FIX_TOOLS],
95
+ toolHints: ["search"],
96
+ });
97
+ const validation = await evaluateReviewFix(api, { issue, context, baseline, isolated });
98
+ return {
99
+ findingId: issue.id,
100
+ result: isolated.result,
101
+ patch: isolated.patch,
102
+ changed: isolated.changed,
103
+ validation,
104
+ };
105
+ }),
106
+ { settled: true },
107
+ );
108
+
109
+ const fixes = settled.map((entry, index): ReviewFixOutcome =>
110
+ entry.ok ? entry.value : { findingId: issues[index]!.id, error: entry.error },
111
+ );
112
+ const successful = fixes.filter(isReviewFixPreview);
113
+ const count = (status: PatchValidation["status"]) => successful.filter((fix) => fix.validation.status === status).length;
114
+ const failed = fixes.length - successful.length;
115
+
116
+ return {
117
+ summary: `${count("verified")} verified candidate(s); ${count("rejected")} rejected; ${count("blocked")} blocked; ${count("no-patch")} no-patch; ${failed} attempt(s) failed.`,
118
+ fixes,
119
+ };
120
+ }
121
+
122
+ function isReviewFixPreview(outcome: ReviewFixOutcome): outcome is ReviewFixPreview {
123
+ return "patch" in outcome;
124
+ }
125
+
126
+ async function evaluateReviewFix(api: ReviewFixWorkflowApi, input: {
127
+ issue: ReviewIssue; context: ReviewContext | undefined; baseline: WorktreeBaseline; isolated: IsolatedAgentResult<string>;
128
+ }): Promise<PatchValidation> {
129
+ const { issue, context, baseline, isolated } = input;
130
+ const validation = initialPatchValidation(baseline, isolated.baselineOid, isolated.patch);
131
+ if (!isolated.patch.trim()) return validation;
132
+ if (!isolated.baselineOid) return { ...validation, reason: "Candidate has no recorded baseline identity." };
133
+ if (!context?.snapshot) return { ...validation, reason: "Reviewed snapshot identity is unavailable." };
134
+ if (validation.baselineFingerprint !== context.snapshot.baselineFingerprint) {
135
+ return { ...validation, status: "rejected", reason: "Stale reviewed baseline identity." };
136
+ }
137
+ try {
138
+ const evaluated = await api.agent(
139
+ `Independently evaluate a candidate repair. Your fresh worktree contains the exact reviewed baseline plus the captured patch. The implementer's report is not validation evidence. Inspect the finding, callers and tests. Reject incorrect repairs; return blocked if required validation is unavailable. Select at most six focused deterministic checks with executable and argument arrays. Require at least one meaningful behavior check. If a regression test is applicable, supply a test-only baselinePatch and specific expectedFailure so the engine can prove it fails before the repair and passes after. Do not edit, install dependencies, commit or change branches. The engine will execute checks independently.\nFinding: ${JSON.stringify(serializeReviewIssue(issue))}\nBaseline: ${isolated.baselineOid}\nPatch SHA-256: ${validation.patchHash}\nPatch:\n${isolated.patch}`,
140
+ { isolation: "worktree", candidatePatch: { baselineOid: isolated.baselineOid, patch: isolated.patch },
141
+ label: `evaluate:${issue.id}`, phase: "Validate patch previews", profile: "medium", resume: "off",
142
+ tools: ["read", "bash", "grep", "find", "ls"], toolHints: ["search"], schema: PatchEvaluationSchema },
143
+ );
144
+ if (evaluated.baselineOid !== isolated.baselineOid || evaluated.patch !== isolated.patch) {
145
+ return { ...validation, status: "rejected", reason: "Evaluator changed the candidate or used a different baseline.", evaluation: evaluated.result };
146
+ }
147
+ return await validateCandidatePatch({ cwd: api.cwd, baseline, expectedFingerprint: context.snapshot.baselineFingerprint,
148
+ baselineOid: isolated.baselineOid, patch: isolated.patch, evaluation: evaluated.result, signal: api.signal });
149
+ } catch (error) {
150
+ if (isFatalWorkflowError(error, api.signal)) throw error;
151
+ return { ...validation, status: "blocked", reason: unknownErrorMessage(error) };
152
+ }
153
+ }
@@ -0,0 +1,133 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
+ import { truncateDisplay, type WorkflowThemeColor } from "../ui/workflow-format.ts";
4
+ import { formatIssueLocation, type ReviewIssue } from "./review-issues.ts";
5
+
6
+ export interface RenderIssuesTableOptions {
7
+ readonly maxRows?: number;
8
+ }
9
+
10
+ const DEFAULT_MAX_ROWS = 12;
11
+ const ID_WIDTH = 4;
12
+ const SEVERITY_WIDTH = 6;
13
+ const CONFIDENCE_WIDTH = 6;
14
+ const CATEGORY_WIDTH = 8;
15
+ const LOCATION_WIDTH = 30;
16
+ const SUMMARY_WIDTH = 58;
17
+
18
+ export function renderIssuesTable(issues: readonly ReviewIssue[], theme: Theme, options: RenderIssuesTableOptions = {}): string {
19
+ const maxRows = options.maxRows ?? DEFAULT_MAX_ROWS;
20
+ const visible = maxRows >= 0 ? issues.slice(0, maxRows) : issues;
21
+ const lines = [
22
+ renderRow(["ID", "Sev", "Conf", "Cat", "Location", "Summary"], theme.fg("dim", "│"), theme, true),
23
+ theme.fg("dim", renderSeparator()),
24
+ ];
25
+
26
+ for (const issue of visible) {
27
+ lines.push(
28
+ renderRow(
29
+ [
30
+ issue.id,
31
+ issue.finding.severity,
32
+ issue.finding.confidence,
33
+ issue.finding.category,
34
+ formatIssueLocation(issue),
35
+ issue.finding.summary,
36
+ ],
37
+ theme.fg("dim", "│"),
38
+ theme,
39
+ false,
40
+ issue,
41
+ ),
42
+ );
43
+ }
44
+
45
+ if (issues.length > visible.length) {
46
+ lines.push(theme.fg("dim", `… ${issues.length - visible.length} more finding(s)`));
47
+ }
48
+
49
+ return lines.join("\n");
50
+ }
51
+
52
+ export function renderIssueDetails(issue: ReviewIssue, theme: Theme): string {
53
+ return renderIssueDetailLines(issue, theme, 120).join("\n");
54
+ }
55
+
56
+ export function renderIssueDetailLines(issue: ReviewIssue, theme: Theme, width: number): string[] {
57
+ const finding = issue.finding;
58
+ const metadata = `${finding.category} · severity ${finding.severity} · confidence ${finding.confidence}${finding.verdict ? ` · ${finding.verdict}` : ""}`;
59
+ const lines = [`${theme.fg("accent", issue.id)} ${theme.fg("text", finding.summary)}`];
60
+ lines.push(...fieldLines("Metadata", metadata, width, theme));
61
+ if (finding.sourceCandidateIds?.length) lines.push(...fieldLines("Sources", finding.sourceCandidateIds.join(", "), width, theme));
62
+ lines.push(...fieldLines("Location", formatIssueLocation(issue), width, theme, "accent"));
63
+ lines.push(...fieldLines("Impact", finding.impact, width, theme));
64
+ lines.push(...fieldLines("Evidence", finding.evidence.join("; ") || "(none cited)", width, theme));
65
+ lines.push(...fieldLines("Recommendation", finding.recommendation, width, theme));
66
+ return lines.flatMap((line) => wrapTextWithAnsi(line, Math.max(10, width)));
67
+ }
68
+
69
+ function fieldLines(label: string, value: string, width: number, theme: Theme, valueColor: WorkflowThemeColor = "muted"): string[] {
70
+ const prefix = ` ${theme.fg("dim", `${label}:`)}`;
71
+ const separator = " ";
72
+ const valueText = theme.fg(valueColor, value);
73
+ const available = Math.max(10, width - visibleWidth(`${label}: `) - 2);
74
+ const wrapped = wrapTextWithAnsi(valueText, available);
75
+ if (wrapped.length === 0) return [`${prefix}${separator}`];
76
+ const continuation = " ".repeat(visibleWidth(`${label}: `) + 2);
77
+ return wrapped.map((line, index) => (index === 0 ? `${prefix}${separator}${line}` : `${continuation}${line}`));
78
+ }
79
+
80
+ function renderRow(
81
+ cells: readonly [string, string, string, string, string, string],
82
+ separator: string,
83
+ theme: Theme,
84
+ header: boolean,
85
+ issue?: ReviewIssue,
86
+ ): string {
87
+ const rendered = [
88
+ cell(cells[0], ID_WIDTH),
89
+ colorCell(cell(cells[1], SEVERITY_WIDTH), header ? "dim" : severityColor(issue?.finding.severity ?? "low"), theme),
90
+ colorCell(cell(cells[2], CONFIDENCE_WIDTH), header ? "dim" : confidenceColor(issue?.finding.confidence ?? "low"), theme),
91
+ cell(cells[3], CATEGORY_WIDTH),
92
+ cell(cells[4], LOCATION_WIDTH),
93
+ cell(cells[5], SUMMARY_WIDTH),
94
+ ];
95
+ if (header) {
96
+ return rendered.map((entry) => theme.fg("dim", entry)).join(` ${separator} `);
97
+ }
98
+ return rendered.join(` ${separator} `);
99
+ }
100
+
101
+ function cell(value: string, width: number): string {
102
+ return truncateDisplay(value, width).padEnd(width, " ");
103
+ }
104
+
105
+ function colorCell(value: string, color: WorkflowThemeColor, theme: Theme): string {
106
+ return theme.fg(color, value);
107
+ }
108
+
109
+ function renderSeparator(): string {
110
+ return [ID_WIDTH, SEVERITY_WIDTH, CONFIDENCE_WIDTH, CATEGORY_WIDTH, LOCATION_WIDTH, SUMMARY_WIDTH].map((width) => "─".repeat(width)).join("─┼─");
111
+ }
112
+
113
+ function severityColor(severity: ReviewIssue["finding"]["severity"]): WorkflowThemeColor {
114
+ switch (severity) {
115
+ case "high":
116
+ return "error";
117
+ case "medium":
118
+ return "warning";
119
+ case "low":
120
+ return "muted";
121
+ }
122
+ }
123
+
124
+ function confidenceColor(confidence: ReviewIssue["finding"]["confidence"]): WorkflowThemeColor {
125
+ switch (confidence) {
126
+ case "high":
127
+ return "success";
128
+ case "medium":
129
+ return "warning";
130
+ case "low":
131
+ return "muted";
132
+ }
133
+ }
@@ -0,0 +1,38 @@
1
+ import { serializeReviewIssue, type ReviewIssue } from "./review-issues.ts";
2
+ import type { ReviewContext } from "./review-report.ts";
3
+ import { serializeReviewContext } from "./review-report.ts";
4
+
5
+ export function buildCommentHandoffPrompt(
6
+ issues: readonly ReviewIssue[],
7
+ context: ReviewContext | undefined,
8
+ verifiedHead: string,
9
+ reason: string,
10
+ ): string {
11
+ return `Use the workflow-code-review-actions skill if available.
12
+
13
+ Mode: post inline GitHub PR comments for selected code-review findings.
14
+
15
+ Fallback reason: ${reason}
16
+
17
+ Selected findings JSON:
18
+ \`\`\`json
19
+ ${JSON.stringify(toHandoffPayload(issues, context))}
20
+ \`\`\`
21
+
22
+ Instructions:
23
+ - Do not edit files or make code changes.
24
+ - Prefer installed GitHub MCP/tools if present; otherwise use the GitHub CLI (gh).
25
+ - Before posting, resolve the current PR head and require it to equal the verified reviewed head \`${verifiedHead}\`; stop if it differs.
26
+ - With gh, resolve the PR using \`gh pr view\` and \`gh repo view\`, then call \`gh api repos/{owner}/{repo}/pulls/{number}/comments\` with \`commit_id\`, \`path\`, \`line\`, and \`side=RIGHT\`.
27
+ - Do not post duplicate comments.
28
+ - Do not post line-less findings as inline comments.
29
+ - Ask the user if the upstream PR cannot be identified.
30
+ - Summarize posted, skipped, and failed comments when done.`;
31
+ }
32
+
33
+ function toHandoffPayload(issues: readonly ReviewIssue[], context: ReviewContext | undefined) {
34
+ return {
35
+ context: serializeReviewContext(context),
36
+ issues: issues.map(serializeReviewIssue),
37
+ };
38
+ }
@@ -0,0 +1,87 @@
1
+ import type { AdvisoryFinding, AdvisoryReport } from "../advisory-schema.ts";
2
+
3
+ export type ReviewIssueAction = "fix" | "comment" | "close";
4
+
5
+ export interface ReviewIssueSelection {
6
+ readonly action: ReviewIssueAction;
7
+ readonly issueIds: readonly string[];
8
+ }
9
+
10
+ export interface ReviewIssue {
11
+ readonly id: string;
12
+ readonly index: number;
13
+ readonly workflowName: string;
14
+ readonly file?: string;
15
+ readonly line?: number;
16
+ readonly symbol?: string;
17
+ readonly finding: AdvisoryFinding;
18
+ }
19
+
20
+ /** Stable prompt-facing representation shared by review follow-up actions. */
21
+ export interface SerializedReviewIssue {
22
+ readonly id: string;
23
+ readonly summary: string;
24
+ readonly category: string;
25
+ readonly severity: string;
26
+ readonly confidence: string;
27
+ readonly location: {
28
+ readonly file?: string;
29
+ readonly line?: number;
30
+ readonly symbol?: string;
31
+ readonly display: string;
32
+ };
33
+ readonly impact: string;
34
+ readonly evidence: readonly string[];
35
+ readonly recommendation: string;
36
+ }
37
+
38
+ export function toReviewIssues(name: string, report: Pick<AdvisoryReport, "findings">): ReviewIssue[] {
39
+ return report.findings.map((finding, index) => {
40
+ const location = finding.reviewAnchor ?? finding.locations[0];
41
+ return {
42
+ id: formatIssueId(index),
43
+ index,
44
+ workflowName: name,
45
+ file: location?.file,
46
+ line: location?.line,
47
+ symbol: location?.symbol,
48
+ finding,
49
+ };
50
+ });
51
+ }
52
+
53
+ export function formatIssueLocation(issue: ReviewIssue): string {
54
+ if (!issue.file) return "(no location)";
55
+ const line = issue.line != null ? `:${issue.line}` : "";
56
+ const symbol = issue.symbol ? ` (${issue.symbol})` : "";
57
+ return `${issue.file}${line}${symbol}`;
58
+ }
59
+
60
+ export function serializeReviewIssue(issue: ReviewIssue): SerializedReviewIssue {
61
+ return {
62
+ id: issue.id,
63
+ summary: issue.finding.summary,
64
+ category: issue.finding.category,
65
+ severity: issue.finding.severity,
66
+ confidence: issue.finding.confidence,
67
+ location: {
68
+ file: issue.file,
69
+ line: issue.line,
70
+ symbol: issue.symbol,
71
+ display: formatIssueLocation(issue),
72
+ },
73
+ impact: issue.finding.impact,
74
+ evidence: issue.finding.evidence,
75
+ recommendation: issue.finding.recommendation,
76
+ };
77
+ }
78
+
79
+ export function isCommentableIssue(
80
+ issue: ReviewIssue,
81
+ ): issue is ReviewIssue & { readonly file: string; readonly line: number } {
82
+ return typeof issue.file === "string" && issue.file.trim().length > 0 && typeof issue.line === "number" && Number.isFinite(issue.line);
83
+ }
84
+
85
+ function formatIssueId(index: number): string {
86
+ return `R${String(index + 1).padStart(3, "0")}`;
87
+ }
@@ -0,0 +1,42 @@
1
+ import { Type, type Static } from "typebox";
2
+ import { Value } from "typebox/value";
3
+ import { AdvisoryReportWithStatsSchema } from "../advisory-schema.ts";
4
+ import { formatReviewDiffTarget, isReviewDiffTarget, ReviewDiffTargetSchema } from "../review-diff-target.ts";
5
+
6
+ /** Atomic identity of the exact diff and post-change snapshot that were reviewed. */
7
+ export const ReviewSnapshotIdentitySchema = Type.Object({
8
+ diffFingerprint: Type.String({ pattern: "^[0-9a-fA-F]{64}$" }),
9
+ baselineFingerprint: Type.String({ pattern: "^[0-9a-fA-F]{64}$" }),
10
+ });
11
+
12
+ export const ReviewContextSchema = Type.Object({
13
+ workflowName: Type.String(),
14
+ target: Type.String(),
15
+ diffTarget: ReviewDiffTargetSchema,
16
+ files: Type.Array(Type.String()),
17
+ summary: Type.Optional(Type.String()),
18
+ snapshot: Type.Optional(ReviewSnapshotIdentitySchema),
19
+ });
20
+
21
+ export const ReviewReportSchema = Type.Object({
22
+ ...AdvisoryReportWithStatsSchema.properties,
23
+ reviewContext: Type.Optional(ReviewContextSchema),
24
+ });
25
+
26
+ export type ReviewSnapshotIdentity = Static<typeof ReviewSnapshotIdentitySchema>;
27
+ export type ReviewContext = Static<typeof ReviewContextSchema>;
28
+ export type ReviewReport = Static<typeof ReviewReportSchema>;
29
+
30
+ export function isReviewReport(value: unknown): value is ReviewReport {
31
+ if (!Value.Check(ReviewReportSchema, value)) return false;
32
+ return value.reviewContext === undefined || isReviewContext(value.reviewContext);
33
+ }
34
+
35
+ export function isReviewContext(value: unknown): value is ReviewContext {
36
+ return Value.Check(ReviewContextSchema, value) && isReviewDiffTarget(value.diffTarget);
37
+ }
38
+
39
+ /** Prompt-facing context retains the canonical display command without persisting duplicate identity. */
40
+ export function serializeReviewContext(context: ReviewContext | undefined): (ReviewContext & { readonly diffCommand: string }) | undefined {
41
+ return context ? { ...context, diffCommand: formatReviewDiffTarget(context.diffTarget) } : undefined;
42
+ }
@@ -0,0 +1,52 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { WorkflowRunOptions } from "../types.ts";
3
+ import { toReviewIssues, type ReviewIssue, type ReviewIssueSelection } from "./review-issues.ts";
4
+ import { isReviewReport, type ReviewReport } from "./review-report.ts";
5
+ import { ReviewResultsViewer } from "./review-results-viewer.ts";
6
+ import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "../ui/workflow-viewer-layout.ts";
7
+
8
+ export type ReviewResultsPresentationDecision =
9
+ | {
10
+ readonly kind: "send";
11
+ readonly reason: "not-code-review" | "not-advisory" | "no-findings" | "disabled" | "not-tui" | "not-requested";
12
+ }
13
+ | { readonly kind: "open"; readonly issues: readonly ReviewIssue[] };
14
+
15
+ export interface ReviewResultsDecisionInput {
16
+ readonly workflowName: string;
17
+ readonly result: unknown;
18
+ readonly mode?: string;
19
+ readonly hasUI: boolean;
20
+ readonly resultViewer?: WorkflowRunOptions["resultViewer"];
21
+ }
22
+
23
+ export function codeReviewReport(workflowName: string, result: unknown): ReviewReport | undefined {
24
+ return workflowName === "code-review" && isReviewReport(result) ? result : undefined;
25
+ }
26
+
27
+ export interface ReviewResultsViewerContext {
28
+ readonly ui: Pick<ExtensionContext["ui"], "custom">;
29
+ }
30
+
31
+ export function decideReviewResultsPresentation(input: ReviewResultsDecisionInput): ReviewResultsPresentationDecision {
32
+ if (input.workflowName !== "code-review") return { kind: "send", reason: "not-code-review" };
33
+ const report = codeReviewReport(input.workflowName, input.result);
34
+ if (!report) return { kind: "send", reason: "not-advisory" };
35
+ if (report.findings.length === 0) return { kind: "send", reason: "no-findings" };
36
+ if (input.resultViewer === "skip") return { kind: "send", reason: "disabled" };
37
+ if (input.mode !== "tui" || !input.hasUI) return { kind: "send", reason: "not-tui" };
38
+
39
+ if (input.resultViewer !== "open") return { kind: "send", reason: "not-requested" };
40
+
41
+ return { kind: "open", issues: toReviewIssues(input.workflowName, report) };
42
+ }
43
+
44
+ export async function showReviewResultsViewer(
45
+ ctx: ReviewResultsViewerContext,
46
+ issues: readonly ReviewIssue[],
47
+ ): Promise<ReviewIssueSelection> {
48
+ return await ctx.ui.custom<ReviewIssueSelection>(
49
+ (tui, theme, _keybindings, done) => new ReviewResultsViewer(issues, "code-review", tui, theme, done),
50
+ WORKFLOW_VIEWER_OVERLAY_OPTIONS,
51
+ );
52
+ }