@jameslovespancakes/pi-plus 1.0.11 → 1.0.13

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 (133) hide show
  1. package/README.md +67 -22
  2. package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
  3. package/package.json +9 -4
  4. package/src/core/accounts/oauth-pool.ts +137 -0
  5. package/src/core/accounts/registry.ts +12 -25
  6. package/src/core/accounts/routing.ts +144 -0
  7. package/src/core/anthropic/client-identity.ts +9 -64
  8. package/src/core/anthropic/identity.ts +54 -0
  9. package/src/core/anthropic/oauth.ts +3 -0
  10. package/src/core/anthropic/quota.ts +232 -78
  11. package/src/core/anthropic/routing.ts +40 -115
  12. package/src/core/anthropic/store.ts +11 -10
  13. package/src/core/catalog/quality.ts +33 -15
  14. package/src/core/codex/quota.ts +46 -8
  15. package/src/core/codex/store.ts +29 -16
  16. package/src/core/config.ts +16 -17
  17. package/src/core/store.ts +13 -12
  18. package/src/domains/agents/format.ts +97 -0
  19. package/src/domains/agents/index.ts +58 -47
  20. package/src/domains/compact/archive.ts +140 -0
  21. package/src/domains/compact/chunking.ts +166 -0
  22. package/src/domains/compact/index.ts +452 -0
  23. package/src/domains/compact/jev.ts +242 -0
  24. package/src/domains/compact/policy.ts +255 -0
  25. package/src/domains/compact/types.ts +79 -0
  26. package/src/domains/models/provider-picker.ts +1 -1
  27. package/src/domains/setup/index.ts +23 -23
  28. package/src/domains/subscriptions/accounts-picker.ts +61 -40
  29. package/src/domains/subscriptions/accounts.ts +53 -36
  30. package/src/domains/subscriptions/footer.ts +48 -29
  31. package/src/domains/subscriptions/index.ts +11 -27
  32. package/src/domains/subscriptions/provider.ts +79 -103
  33. package/src/domains/subscriptions/providers/anthropic.ts +42 -22
  34. package/src/domains/subscriptions/providers/codex.ts +146 -113
  35. package/src/domains/subscriptions/providers/hosted.ts +18 -0
  36. package/src/domains/subscriptions/providers/oauth-pool.ts +325 -0
  37. package/src/domains/subscriptions/routing.ts +19 -11
  38. package/src/domains/workflows/LICENSE.md +21 -0
  39. package/src/domains/workflows/index.ts +836 -0
  40. package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
  41. package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
  42. package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
  43. package/src/domains/workflows/runtime/agent-attempt.ts +177 -0
  44. package/src/domains/workflows/runtime/agent-failure.ts +14 -0
  45. package/src/domains/workflows/runtime/agent-limits.ts +66 -0
  46. package/src/domains/workflows/runtime/agent-replay.ts +399 -0
  47. package/src/domains/workflows/runtime/agent-retry.ts +116 -0
  48. package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
  49. package/src/domains/workflows/runtime/agent-runner.ts +225 -0
  50. package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
  51. package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
  52. package/src/domains/workflows/runtime/agent-session.ts +382 -0
  53. package/src/domains/workflows/runtime/agent-skills.ts +270 -0
  54. package/src/domains/workflows/runtime/agent-workspace.ts +79 -0
  55. package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
  56. package/src/domains/workflows/runtime/background-workflows.ts +492 -0
  57. package/src/domains/workflows/runtime/budget.ts +53 -0
  58. package/src/domains/workflows/runtime/cancellation.ts +87 -0
  59. package/src/domains/workflows/runtime/command-completions.ts +36 -0
  60. package/src/domains/workflows/runtime/concurrency.ts +403 -0
  61. package/src/domains/workflows/runtime/debug.ts +3 -0
  62. package/src/domains/workflows/runtime/diff-capture.ts +81 -0
  63. package/src/domains/workflows/runtime/discovery.ts +137 -0
  64. package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
  65. package/src/domains/workflows/runtime/dynamax.ts +330 -0
  66. package/src/domains/workflows/runtime/engine.ts +579 -0
  67. package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
  68. package/src/domains/workflows/runtime/finalizers.ts +66 -0
  69. package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
  70. package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
  71. package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
  72. package/src/domains/workflows/runtime/journal.ts +313 -0
  73. package/src/domains/workflows/runtime/model-profiles.ts +310 -0
  74. package/src/domains/workflows/runtime/options.ts +157 -0
  75. package/src/domains/workflows/runtime/perf.ts +146 -0
  76. package/src/domains/workflows/runtime/pi-compat.ts +32 -0
  77. package/src/domains/workflows/runtime/process-runner.ts +260 -0
  78. package/src/domains/workflows/runtime/progress-types.ts +59 -0
  79. package/src/domains/workflows/runtime/progress.ts +400 -0
  80. package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
  81. package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
  82. package/src/domains/workflows/runtime/research-contract.ts +89 -0
  83. package/src/domains/workflows/runtime/research-evidence.ts +289 -0
  84. package/src/domains/workflows/runtime/resume-context.ts +751 -0
  85. package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
  86. package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
  87. package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
  88. package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
  89. package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
  90. package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
  91. package/src/domains/workflows/runtime/review/review-format.ts +133 -0
  92. package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
  93. package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
  94. package/src/domains/workflows/runtime/review/review-report.ts +42 -0
  95. package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
  96. package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
  97. package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
  98. package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
  99. package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
  100. package/src/domains/workflows/runtime/session-identity.ts +6 -0
  101. package/src/domains/workflows/runtime/structured-output.ts +32 -0
  102. package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
  103. package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
  104. package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
  105. package/src/domains/workflows/runtime/types.ts +255 -0
  106. package/src/domains/workflows/runtime/ui/display-text.ts +28 -0
  107. package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
  108. package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
  109. package/src/domains/workflows/runtime/ui/workflow-inspector.ts +435 -0
  110. package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
  111. package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
  112. package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
  113. package/src/domains/workflows/runtime/unknown-error.ts +8 -0
  114. package/src/domains/workflows/runtime/usage.ts +341 -0
  115. package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
  116. package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
  117. package/src/domains/workflows/runtime/workflow-module.ts +75 -0
  118. package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
  119. package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
  120. package/src/domains/workflows/runtime/workflow-run-history.ts +164 -0
  121. package/src/domains/workflows/runtime/workflow-run-record.ts +636 -0
  122. package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
  123. package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
  124. package/src/domains/workflows/runtime/workflows.ts +40 -0
  125. package/src/domains/workflows/runtime/worktree.ts +615 -0
  126. package/src/domains/workflows/workflows/code-review.ts +232 -0
  127. package/src/domains/workflows/workflows/diagnose.ts +154 -0
  128. package/src/domains/workflows/workflows/perf-review.ts +149 -0
  129. package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
  130. package/src/domains/workflows/workflows/research.ts +169 -0
  131. package/src/services/usage-service.ts +6 -30
  132. package/src/ui/usage-bars.ts +1 -2
  133. package/src/core/codex/oauth.ts +0 -129
@@ -0,0 +1,75 @@
1
+ import { Type, type Static } from "typebox";
2
+ import { collectAdvisoryStage, type AdvisoryStageCoverage } from "./advisory-evidence.ts";
3
+ import { DEFAULT_ADVISORY_TOOLS, DEFAULT_ADVISORY_TOOL_HINTS, type AdvisoryVerified } from "./workflow-advisory-utils.ts";
4
+ import type { WorkflowApi } from "./types.ts";
5
+
6
+ const ChallengeSchema = Type.Object({
7
+ outcome: Type.Union([Type.Literal("counterexample"), Type.Literal("alternative-explanation"), Type.Literal("supports-original"), Type.Literal("no-counterexample")]),
8
+ evidence: Type.Array(Type.String()),
9
+ experiment: Type.String({ description: "Smallest distinguishing experiment, with observed result if actually run." }),
10
+ });
11
+ const AdjudicationSchema = Type.Object({
12
+ outcome: Type.Union([Type.Literal("upheld"), Type.Literal("refuted"), Type.Literal("unresolved")]),
13
+ evidence: Type.Array(Type.String()),
14
+ reason: Type.String(),
15
+ });
16
+ export type ChallengeRecord =
17
+ | { status: "complete"; challenge: Static<typeof ChallengeSchema>; adjudication: Static<typeof AdjudicationSchema> }
18
+ | { status: "failed"; challenge?: Static<typeof ChallengeSchema> };
19
+ export interface AdvisoryChallengeOptions {
20
+ maxChallenges: number;
21
+ shouldChallenge?: (finding: AdvisoryVerified) => boolean;
22
+ }
23
+
24
+ /** Opt in with --challenge or --challenge=N (hard bound of 10). */
25
+ export function parseChallengeArgs(args: string): { args: string; options: AdvisoryChallengeOptions } {
26
+ let maxChallenges = 0;
27
+ const remaining = args.replace(/(?:^|\s)--challenge(?:=([^\s]*))?(?=\s|$)/g, (_match, limit: string | undefined) => {
28
+ if (limit !== undefined && !/^\d+$/.test(limit)) {
29
+ throw new Error(`Invalid --challenge value "${limit}". Use --challenge or --challenge=N with a non-negative integer; zero disables challenges.`);
30
+ }
31
+ maxChallenges = Math.min(10, limit === undefined ? 3 : Number(limit));
32
+ return " ";
33
+ });
34
+ return { args: remaining.trim(), options: { maxChallenges } };
35
+ }
36
+
37
+ export function needsChallenge(finding: AdvisoryVerified): boolean {
38
+ return finding.verdict === "PLAUSIBLE" || finding.verdict === "NOT_SUBSTANTIATED" || finding.evidence.length === 0 ||
39
+ /\b(security|concurren\w*|race|persist\w*|cancel\w*|retr(?:y|ies)|data loss|corrupt\w*|high impact)\b/i.test(`${finding.category} ${finding.summary} ${finding.impact}`);
40
+ }
41
+
42
+ /** A bounded recipe using existing agent/parallel calls, not a new runtime primitive. */
43
+ export async function challengeFindings<T extends AdvisoryVerified>(
44
+ api: Pick<WorkflowApi, "agent" | "parallel">,
45
+ findings: T[],
46
+ context: string,
47
+ options: AdvisoryChallengeOptions,
48
+ coverage: AdvisoryStageCoverage[],
49
+ ): Promise<T[]> {
50
+ const limit = Number.isFinite(options.maxChallenges) ? Math.max(0, Math.min(10, Math.trunc(options.maxChallenges))) : 0;
51
+ if (limit === 0) return findings;
52
+ const selected = findings.filter((finding) => finding.verdict !== "REFUTED" && (options.shouldChallenge ?? needsChallenge)(finding)).slice(0, limit);
53
+ const replacements = new Map<string, T>();
54
+ await collectAdvisoryStage(api, "Challenge", selected.map((finding) => ({ id: finding.candidateId, run: async () => {
55
+ // Preserve the candidate as unresolved if either independent stage fails.
56
+ replacements.set(finding.candidateId, { ...finding, verdict: "NOT_SUBSTANTIATED", challenge: { status: "failed" } });
57
+ const challenge = await api.agent(
58
+ `Assume this finding is a false positive. Try to DISPROVE it. Find the strongest concrete counterexample or alternative root cause. Inspect callers, invariants, tests and control flow. For a repair, seek an input, race or error path that still fails. State the smallest experiment distinguishing explanations. Do not edit files or claim tests you did not run. No counterexample found is not proof.\n\nExact review context:\n${context}\n\nCandidate and verifier evidence:\n${JSON.stringify(finding)}`,
59
+ { label: `challenge:${finding.candidateId}`, phase: "Challenge", profile: "medium", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, schema: ChallengeSchema },
60
+ );
61
+ replacements.set(finding.candidateId, { ...finding, verdict: "NOT_SUBSTANTIATED", challenge: { status: "failed", challenge } });
62
+ const adjudication = await api.agent(
63
+ `Adjudicate the original finding, independent verifier evidence and falsification attempt below. Preserve unresolved conflict; do not force consensus. A missing counterexample alone cannot upgrade a plausible claim. Cite concrete evidence and observed test results; never invent experiments.\nContext:\n${context}\nOriginal and verifier:\n${JSON.stringify(finding)}\nChallenger:\n${JSON.stringify(challenge)}`,
64
+ { label: `adjudicate:${finding.candidateId}`, phase: "Challenge", profile: "medium", tools: [], schema: AdjudicationSchema },
65
+ );
66
+ replacements.set(finding.candidateId, {
67
+ ...finding,
68
+ verdict: adjudication.outcome === "refuted" ? "REFUTED" : adjudication.outcome === "unresolved" ? "NOT_SUBSTANTIATED" : finding.verdict,
69
+ evidence: [...finding.evidence, ...challenge.evidence, ...(challenge.experiment ? [challenge.experiment] : []), ...adjudication.evidence, adjudication.reason],
70
+ challenge: { status: "complete", challenge, adjudication },
71
+ });
72
+ return finding.candidateId;
73
+ } })), coverage);
74
+ return findings.map((finding) => replacements.get(finding.candidateId) ?? finding);
75
+ }
@@ -0,0 +1,90 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Type, type Static } from "typebox";
3
+ import { AdvisorySeveritySchema, type IdentifiedAdvisoryCandidate, type AdvisoryCandidate, type AdvisoryReport } from "./advisory-schema.ts";
4
+ import type { WorkflowApi } from "./types.ts";
5
+
6
+ export interface AdvisoryStageCoverage {
7
+ stage: string;
8
+ expected: number;
9
+ completed: number;
10
+ failed: number;
11
+ failures: { branch: string; reason: string; candidate?: AdvisoryCandidate }[];
12
+ }
13
+
14
+ /** Settled results are required here: a missing branch is not a negative finding. */
15
+ export async function collectAdvisoryStage<T>(
16
+ api: Pick<WorkflowApi, "parallel">,
17
+ stage: string,
18
+ branches: { id: string; candidate?: AdvisoryCandidate; run(): Promise<T> }[],
19
+ coverage: AdvisoryStageCoverage[],
20
+ ): Promise<T[]> {
21
+ const results = await api.parallel(branches.map((branch) => branch.run), { settled: true });
22
+ const failures = results.flatMap((result, index) => result.ok ? [] : [{ branch: branches[index]!.id, reason: result.error.message, ...(branches[index]!.candidate ? { candidate: branches[index]!.candidate } : {}) }]);
23
+ coverage.push({ stage, expected: branches.length, completed: results.length - failures.length, failed: failures.length, failures });
24
+ return results.flatMap((result) => result.ok ? [result.value] : []);
25
+ }
26
+
27
+ export function identifyCandidates(candidates: readonly AdvisoryCandidate[], lens: string): IdentifiedAdvisoryCandidate[] {
28
+ return candidates.map((candidate, index) => {
29
+ const candidateId = createHash("sha256").update(JSON.stringify([lens, index, candidate])).digest("hex").slice(0, 20);
30
+ // Identities are assigned by the workflow, never accepted from a finder.
31
+ return { ...candidate, candidateId, sourceCandidateIds: [candidateId] };
32
+ });
33
+ }
34
+
35
+ function normalizedSummary(summary: string): string {
36
+ return summary.toLowerCase().replace(/\s+/g, " ").trim().replace(/[.!?]+$/, "");
37
+ }
38
+
39
+ function candidateDedupKey(candidate: AdvisoryCandidate): string {
40
+ const location = candidate.locations[0];
41
+ return JSON.stringify([candidate.category, location?.file.replace(/^\.\//, "").replace(/^[ab]\//, ""),
42
+ location?.symbol ?? location?.line ?? null, normalizedSummary(candidate.summary)]);
43
+ }
44
+
45
+ /** Collapse only equivalent claims at the same anchor; keep every discovery ID and location. */
46
+ export function dedupeCandidates<T extends IdentifiedAdvisoryCandidate>(candidates: readonly T[]): T[] {
47
+ const seen = new Map<string, T>();
48
+ for (const candidate of candidates) {
49
+ const key = candidateDedupKey(candidate);
50
+ const previous = seen.get(key);
51
+ if (!previous) seen.set(key, { ...candidate });
52
+ else {
53
+ previous.sourceCandidateIds = [...new Set([...previous.sourceCandidateIds, ...candidate.sourceCandidateIds])];
54
+ previous.locations = uniqueLocations([...previous.locations, ...candidate.locations]);
55
+ previous.impact = [...new Set([previous.impact, candidate.impact])].join("\n");
56
+ previous.discoveryEvidence = [...new Set([...(previous.discoveryEvidence ?? []), ...(candidate.discoveryEvidence ?? [])])];
57
+ }
58
+ }
59
+ return [...seen.values()];
60
+ }
61
+
62
+ export function uniqueLocations(locations: AdvisoryCandidate["locations"]): AdvisoryCandidate["locations"] {
63
+ return [...new Map(locations.map((location) => [JSON.stringify(location), location])).values()];
64
+ }
65
+
66
+ // Synthesis selects evidence records. It has no fields with which to replace their evidence.
67
+ export const AdvisorySynthesisSchema = Type.Object({
68
+ summary: Type.String(),
69
+ findings: Type.Array(Type.Object({
70
+ sourceCandidateIds: Type.Array(Type.String(), { minItems: 1 }),
71
+ severity: AdvisorySeveritySchema,
72
+ recommendation: Type.String(),
73
+ })),
74
+ nextSteps: Type.Array(Type.String()),
75
+ });
76
+ export type AdvisorySynthesis = Static<typeof AdvisorySynthesisSchema>;
77
+ export const SYNTHESIS_ID_INSTRUCTIONS = "Select or merge findings only by sourceCandidateIds shown below. Return severity and an advisory recommendation per selection. Never select an ID absent from these verified records. ";
78
+
79
+ export function withAdvisoryCoverage<T extends AdvisoryReport>(report: T, coverage: AdvisoryStageCoverage[]) {
80
+ const incomplete = coverage.some((stage) => stage.failed > 0);
81
+ const gaps = coverage.flatMap((stage) => stage.failures.map((failure) => `${stage.stage}/${failure.branch}: ${failure.reason}`));
82
+ return {
83
+ ...report,
84
+ status: incomplete ? "incomplete" as const : "complete" as const,
85
+ summary: incomplete ? `Incomplete review: ${gaps.length} branch(es) failed. ${report.findings.length} finding(s) available; no clean conclusion is possible.` : report.summary,
86
+ nextSteps: incomplete ? ["Inspect failed branches and rerun missing work.", ...(report.findings.length > 0 ? report.nextSteps : [])] : report.nextSteps,
87
+ coverage,
88
+ gaps,
89
+ };
90
+ }
@@ -0,0 +1,83 @@
1
+ import { Type, type Static } from "typebox";
2
+ import { Value } from "typebox/value";
3
+
4
+ export const AdvisorySeveritySchema = Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
5
+ description: "Impact level of an advisory finding.",
6
+ });
7
+
8
+ export const AdvisoryConfidenceSchema = Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
9
+ description: "Confidence that the finding is actionable.",
10
+ });
11
+
12
+ export const AdvisoryVerifierVerdictSchema = Type.Union([Type.Literal("CONFIRMED"), Type.Literal("PLAUSIBLE"), Type.Literal("REFUTED"), Type.Literal("NOT_SUBSTANTIATED")], {
13
+ description: "Verifier judgment for a candidate finding.",
14
+ });
15
+
16
+ export const AdvisoryLocationSchema = Type.Object({
17
+ file: Type.String({ description: "Repository-relative file path." }),
18
+ line: Type.Optional(Type.Number({ description: "Relevant 1-based line number when known." })),
19
+ symbol: Type.Optional(Type.String({ description: "Relevant function, class, type, or other symbol when known." })),
20
+ });
21
+
22
+ export const AdvisoryCandidateSchema = Type.Object({
23
+ discoveryEvidence: Type.Optional(Type.Array(Type.String())),
24
+ reviewAnchor: Type.Optional(AdvisoryLocationSchema),
25
+ summary: Type.String({ description: "One-line candidate finding or hypothesis." }),
26
+ category: Type.String({ description: "Workflow-specific category such as bug, duplication, root-cause, or io." }),
27
+ locations: Type.Array(AdvisoryLocationSchema, { description: "Relevant code or configuration locations." }),
28
+ impact: Type.String({ description: "Concrete consequence if this candidate is real." }),
29
+ recommendation: Type.Optional(Type.String({ description: "Optional first-step recommendation before synthesis." })),
30
+ });
31
+
32
+ export const AdvisoryCandidatesSchema = Type.Object({
33
+ candidates: Type.Array(AdvisoryCandidateSchema, { description: "Candidate advisory findings or hypotheses." }),
34
+ });
35
+
36
+ export const AdvisoryVerdictSchema = Type.Object({
37
+ verdict: AdvisoryVerifierVerdictSchema,
38
+ evidence: Type.Array(Type.String({ description: "Quoted or cited evidence supporting the verdict." })),
39
+ confidence: Type.Optional(AdvisoryConfidenceSchema),
40
+ });
41
+
42
+ export const AdvisoryFindingSchema = Type.Object({
43
+ verdict: Type.Optional(AdvisoryVerifierVerdictSchema),
44
+ sourceCandidateIds: Type.Optional(Type.Array(Type.String())),
45
+ reviewAnchor: Type.Optional(AdvisoryLocationSchema),
46
+ summary: Type.String({ description: "One-line final finding." }),
47
+ category: Type.String({ description: "Workflow-specific category for grouping and display." }),
48
+ severity: AdvisorySeveritySchema,
49
+ confidence: AdvisoryConfidenceSchema,
50
+ locations: Type.Array(AdvisoryLocationSchema, { description: "Relevant code or configuration locations." }),
51
+ evidence: Type.Array(Type.String({ description: "Quoted or cited evidence." })),
52
+ impact: Type.String({ description: "Concrete user, runtime, maintenance, or performance impact." }),
53
+ recommendation: Type.String({ description: "Advisory next action; not an automatic edit." }),
54
+ });
55
+
56
+ export const AdvisoryReportSchema = Type.Object({
57
+ status: Type.Optional(Type.Union([Type.Literal("complete"), Type.Literal("incomplete")])),
58
+ gaps: Type.Optional(Type.Array(Type.String())),
59
+ coverage: Type.Optional(Type.Array(Type.Object({
60
+ stage: Type.String(), expected: Type.Number(), completed: Type.Number(), failed: Type.Number(),
61
+ failures: Type.Array(Type.Object({ branch: Type.String(), reason: Type.String(), candidate: Type.Optional(AdvisoryCandidateSchema) })),
62
+ }))),
63
+ summary: Type.String({ description: "Short overall advisory summary." }),
64
+ findings: Type.Array(AdvisoryFindingSchema, { description: "Verified and ranked advisory findings." }),
65
+ nextSteps: Type.Array(Type.String({ description: "Concrete follow-up commands, inspections, or decisions." })),
66
+ });
67
+
68
+ export const AdvisoryReportWithStatsSchema = Type.Object({
69
+ ...AdvisoryReportSchema.properties,
70
+ stats: Type.Optional(Type.Record(Type.String(), Type.Union([Type.String(), Type.Number()]))),
71
+ });
72
+
73
+ export type AdvisoryLocation = Static<typeof AdvisoryLocationSchema>;
74
+ export type AdvisoryCandidate = Static<typeof AdvisoryCandidateSchema>;
75
+ export type IdentifiedAdvisoryCandidate = AdvisoryCandidate & { candidateId: string; sourceCandidateIds: string[] };
76
+ export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
77
+ export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
78
+ export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
79
+ export type AdvisoryReportWithStats = Static<typeof AdvisoryReportWithStatsSchema>;
80
+
81
+ export function isAdvisoryReport(value: unknown): value is AdvisoryReportWithStats {
82
+ return Value.Check(AdvisoryReportWithStatsSchema, value);
83
+ }
@@ -0,0 +1,177 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { assertWorkflowBudgetAvailable } from "./budget.ts";
3
+ import { isWorkflowPauseError } from "./cancellation.ts";
4
+ import type { WorkflowAgentReservation } from "./agent-limits.ts";
5
+ import {
6
+ type AgentExecutionOptions,
7
+ type AgentRunTags,
8
+ type RunContext,
9
+ } from "./agent-runner-types.ts";
10
+ import {
11
+ captureReplayIdentity,
12
+ captureIsolatedRepositoryAfterSetup,
13
+ captureSharedRepositoryBeforeSetup,
14
+ isReplayEnabled,
15
+ lookupReplayResult,
16
+ validateReplayEvidence,
17
+ validateReplayIdentity,
18
+ type AgentAttemptResult,
19
+ type AgentReplayEvidence,
20
+ type AgentReplayPlan,
21
+ } from "./agent-replay.ts";
22
+ import { openAgentSession, promptAgentSession, type AgentSessionHandle } from "./agent-session.ts";
23
+ import {
24
+ createAgentWorkspace,
25
+ type AgentWorkspace,
26
+ } from "./agent-workspace.ts";
27
+ import type { AgentResumeBaseContext, AgentResumeContext, RepositoryResumeContext } from "./resume-context.ts";
28
+ import { captureRepositoryMutationGuard } from "./resume-context.ts";
29
+ import { unknownErrorMessage } from "./unknown-error.ts";
30
+
31
+ /** Execute one agent attempt. Sessions close immediately; worktrees settle at the workflow boundary. */
32
+ export async function executeAgentAttempt(input: {
33
+ readonly rc: RunContext;
34
+ readonly prompt: string;
35
+ readonly opts: AgentExecutionOptions;
36
+ readonly resumeBaseContext: AgentResumeBaseContext;
37
+ readonly model: Model<Api> | undefined;
38
+ readonly replay: AgentReplayPlan;
39
+ readonly label: string;
40
+ readonly rowId: number;
41
+ readonly tags: AgentRunTags;
42
+ readonly reserveLiveAgent: () => WorkflowAgentReservation;
43
+ }): Promise<AgentAttemptResult> {
44
+ const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags, reserveLiveAgent } = input;
45
+ let repositoryBefore: RepositoryResumeContext | undefined;
46
+ let evidence: AgentReplayEvidence | undefined;
47
+ if (isReplayEnabled(replay)) {
48
+ if (replay.kind === "isolated") {
49
+ const mutationGuard = await captureRepositoryMutationGuard(rc.cwd, rc.signal);
50
+ if (mutationGuard.kind === "unverifiable") {
51
+ repositoryBefore = mutationGuard;
52
+ } else {
53
+ evidence = { kind: "isolated", mutationGuard: mutationGuard.fingerprint };
54
+ }
55
+ } else {
56
+ repositoryBefore = await captureSharedRepositoryBeforeSetup(rc, prompt, opts, replay);
57
+ evidence = { kind: "shared" };
58
+ }
59
+ }
60
+ let workspace: AgentWorkspace | undefined;
61
+ let handle: AgentSessionHandle | undefined;
62
+ let admission: WorkflowAgentReservation | undefined;
63
+ let liveStarted = false;
64
+
65
+ try {
66
+ // Calls with no replay path cannot become cache hits. Reserve their live
67
+ // slot before creating a disposable worktree or model session so a limit
68
+ // error cannot leave behind a fresh, untouched workspace.
69
+ if (!isReplayEnabled(replay)) {
70
+ assertWorkflowBudgetAvailable(rc.budget);
71
+ admission = reserveLiveAgent();
72
+ }
73
+ workspace = await createAgentWorkspace(rc, opts, label);
74
+ if (replay.kind === "isolated" && evidence?.kind === "isolated") {
75
+ if (workspace.kind !== "isolated") throw new Error("Isolated replay created a shared workspace.");
76
+ repositoryBefore = await captureIsolatedRepositoryAfterSetup(rc, prompt, opts, workspace);
77
+ }
78
+ handle = await openAgentSession({ rc, prompt, opts, cwd: workspace.cwd, model, label, tags });
79
+
80
+ let identity: AgentResumeContext | undefined;
81
+ if (isReplayEnabled(replay) && repositoryBefore && evidence) {
82
+ const capture = await captureReplayIdentity({
83
+ rc,
84
+ base: resumeBaseContext,
85
+ repository: repositoryBefore,
86
+ selectedSkills: handle.selectedSkills,
87
+ session: handle.session,
88
+ sessionCwd: workspace.cwd,
89
+ });
90
+ if (capture.kind === "unverifiable") {
91
+ rc.progress.log(`${label}: resume disabled for this call (${capture.reason})`);
92
+ } else {
93
+ identity = capture.identity;
94
+ const cached = await lookupReplayResult({ rc, key: replay.key, identity, opts, workspace });
95
+ if (cached.hit) {
96
+ const contract = await validateReplayIdentity({
97
+ rc,
98
+ identity,
99
+ selectedSkills: handle.selectedSkills,
100
+ session: handle.session,
101
+ sessionCwd: workspace.cwd,
102
+ replay,
103
+ workspace,
104
+ });
105
+ const validation = contract.ok
106
+ ? await validateReplayEvidence(rc, evidence)
107
+ : contract;
108
+ if (validation.ok) {
109
+ return { kind: "cache-hit", result: cached.result, identity, evidence };
110
+ }
111
+ rc.progress.log(`${label}: cached result invalidated (${validation.reason})`);
112
+ } else if (cached.reason) {
113
+ rc.progress.log(`${label}: cached result invalidated (${cached.reason})`);
114
+ }
115
+ }
116
+ }
117
+
118
+ if (!admission) {
119
+ assertWorkflowBudgetAvailable(rc.budget);
120
+ admission = reserveLiveAgent();
121
+ }
122
+ admission.commit();
123
+ liveStarted = true;
124
+ if (workspace.kind === "isolated") rc.worktrees.markRecoverable(workspace.cwd);
125
+ const rawResult = await promptAgentSession({
126
+ rc,
127
+ handle,
128
+ prompt,
129
+ opts,
130
+ label,
131
+ rowId,
132
+ tags,
133
+ });
134
+ const result = await workspace.wrapResult(rawResult);
135
+ if (!identity) return { kind: "live-unrecordable", result };
136
+ if (!isReplayEnabled(replay) || !evidence) throw new Error("Replay identity produced without complete replay evidence.");
137
+
138
+ const contract = await validateReplayIdentity({
139
+ rc,
140
+ identity,
141
+ selectedSkills: handle.selectedSkills,
142
+ session: handle.session,
143
+ sessionCwd: workspace.cwd,
144
+ replay,
145
+ workspace,
146
+ });
147
+ const validation = contract.ok
148
+ ? await validateReplayEvidence(rc, evidence)
149
+ : contract;
150
+ if (validation.ok) {
151
+ return { kind: "live-recordable", result, identity, evidence };
152
+ }
153
+ rc.progress.log(`${label}: read-only resume contract was not recorded (${validation.reason})`);
154
+ return { kind: "live-unrecordable", result };
155
+ } catch (error) {
156
+ if (
157
+ workspace?.kind === "isolated"
158
+ && liveStarted
159
+ && !rc.signal?.aborted
160
+ && !isWorkflowPauseError(error)
161
+ ) {
162
+ rc.worktrees.preserve(workspace.cwd);
163
+ rc.progress.log(`${label}: retained failed worktree ${workspace.cwd}`);
164
+ }
165
+ throw error;
166
+ } finally {
167
+ admission?.release();
168
+ const session = handle?.session;
169
+ if (session) {
170
+ try {
171
+ rc.perf.timeSync("agent.dispose_ms", () => session.dispose(), tags);
172
+ } catch (error) {
173
+ rc.progress.log(`${label}: session cleanup failed (${unknownErrorMessage(error)})`);
174
+ }
175
+ }
176
+ }
177
+ }
@@ -0,0 +1,14 @@
1
+ import { unknownErrorMessage } from "./unknown-error.ts";
2
+
3
+ /** Retains every failed attempt when a later retry is blocked or also fails. */
4
+ export function combinedAgentAttemptError(
5
+ label: string,
6
+ prior: readonly unknown[],
7
+ finalError: unknown,
8
+ ): AggregateError {
9
+ const earlier = prior.map(unknownErrorMessage).join("; ");
10
+ return new AggregateError(
11
+ [...prior, finalError],
12
+ `Workflow agent "${label}" could not complete. Earlier attempt: ${earlier}. Final failure: ${unknownErrorMessage(finalError)}`,
13
+ );
14
+ }
@@ -0,0 +1,66 @@
1
+ import { throwIfAborted } from "./cancellation.ts";
2
+
3
+ export class WorkflowAgentLimitError extends Error {
4
+ override readonly name = "WorkflowAgentLimitError";
5
+ readonly code = "WORKFLOW_AGENT_LIMIT_EXCEEDED";
6
+ readonly maxAgents: number;
7
+
8
+ constructor(maxAgents: number) {
9
+ super(`Workflow live-agent limit of ${maxAgents} has been reached; no new model request was started.`);
10
+ this.maxAgents = maxAgents;
11
+ }
12
+ }
13
+
14
+ export class WorkflowAgentTimeoutError extends Error {
15
+ override readonly name = "WorkflowAgentTimeoutError";
16
+ readonly code = "WORKFLOW_AGENT_TIMEOUT";
17
+ readonly label: string;
18
+ readonly timeoutMs: number;
19
+
20
+ constructor(label: string, timeoutMs: number) {
21
+ super(`Workflow agent "${label}" exceeded its ${timeoutMs}ms duration limit.`);
22
+ this.label = label;
23
+ this.timeoutMs = timeoutMs;
24
+ }
25
+ }
26
+
27
+ /** Shared run-level admission counter. Cache hits do not call admit(). */
28
+ export interface WorkflowAgentReservation {
29
+ /** Permanently count this reservation as a live model attempt. */
30
+ commit(): void;
31
+ /** Return an uncommitted reservation after setup or replay succeeds/fails. */
32
+ release(): void;
33
+ }
34
+
35
+ export class WorkflowAgentLimiter {
36
+ readonly maxAgents: number | null;
37
+ private admitted = 0;
38
+
39
+ constructor(maxAgents: number | null) {
40
+ this.maxAgents = maxAgents;
41
+ }
42
+
43
+ reserve(signal: AbortSignal | undefined): WorkflowAgentReservation {
44
+ throwIfAborted(signal);
45
+ if (this.maxAgents !== null && this.admitted >= this.maxAgents) {
46
+ throw new WorkflowAgentLimitError(this.maxAgents);
47
+ }
48
+ this.admitted++;
49
+ let state: "pending" | "committed" | "released" = "pending";
50
+ return {
51
+ commit() {
52
+ if (state === "released") throw new Error("Cannot commit a released workflow agent reservation.");
53
+ state = "committed";
54
+ },
55
+ release: () => {
56
+ if (state !== "pending") return;
57
+ state = "released";
58
+ this.admitted--;
59
+ },
60
+ };
61
+ }
62
+
63
+ admit(signal: AbortSignal | undefined): void {
64
+ this.reserve(signal).commit();
65
+ }
66
+ }