@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,143 @@
1
+ import { challengeFindings, parseChallengeArgs } from "../runtime/advisory-challenge.ts";
2
+ import { Type } from "typebox";
3
+ import {
4
+ type AdvisoryVerified,
5
+ type AdvisoryLens,
6
+ synthesizeAdvisoryReport,
7
+ finishAdvisoryReport,
8
+ emptyAdvisoryReport,
9
+ formatEvidence,
10
+ formatLocation,
11
+ publishVerifiedKeptProgress,
12
+ runLensVerificationPipeline,
13
+ DEFAULT_ADVISORY_TOOL_HINTS,
14
+ DEFAULT_ADVISORY_TOOLS,
15
+ } from "../runtime/workflow-advisory-utils.ts";
16
+ import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../runtime/types.ts";
17
+
18
+ export const meta: WorkflowMeta = {
19
+ name: "refactor-scout",
20
+ description: "Advisory-only refactor scout: scope → per-lens find → independent verify → synthesize safe refactor opportunities.",
21
+ phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
22
+ };
23
+
24
+ const ScopeSchema = Type.Object({
25
+ target: Type.String({ description: "Verbatim target path, module, or focus area being scouted." }),
26
+ files: Type.Array(Type.String(), { description: "Repository-relative files in scope." }),
27
+ summary: Type.String({ description: "One-paragraph summary of the scoped code." }),
28
+ conventions: Type.Optional(Type.String({ description: "Relevant project conventions from AGENTS.md / docs." })),
29
+ });
30
+
31
+ const REFACTOR_LENSES: AdvisoryLens[] = [
32
+ { label: "duplication", category: "duplication", text: "Repeated logic, copy-pasted structures, or near-duplicate flows that could share one clearer implementation." },
33
+ { label: "complexity", category: "complexity", text: "Oversized functions, tangled control flow, or abstractions that make local reasoning harder than necessary." },
34
+ { label: "type-safety", category: "type-safety", text: "Weak typing, avoidable casts, unchecked shapes, or places stronger types would prevent mistakes." },
35
+ { label: "boundaries", category: "boundary", text: "Leaky module boundaries, misplaced responsibilities, or imports that couple unrelated layers." },
36
+ { label: "dead-code", category: "dead-code", text: "Unused, obsolete, or redundant code paths that can likely be removed safely." },
37
+ { label: "conventions", category: "conventions", text: "Departures from project conventions, naming, dependency rules, or local idioms." },
38
+ ];
39
+
40
+ const PER_LENS = 5;
41
+
42
+ export default async function run(api: WorkflowApi): Promise<unknown> {
43
+ const { agent, phase, log, progress, args } = api;
44
+ const challengeConfig = parseChallengeArgs(args);
45
+ const target = challengeConfig.args.trim() || ".";
46
+ let fileCount = 0;
47
+ let rawCandidateCount = 0;
48
+ let droppedCandidateCount = 0;
49
+ let refutedCandidateCount = 0;
50
+ const makeStats = (verified: number, kept: number): WorkflowRunStats => ({
51
+ files: fileCount,
52
+ candidates: rawCandidateCount,
53
+ verified,
54
+ kept,
55
+ dropped: droppedCandidateCount,
56
+ refuted: refutedCandidateCount,
57
+ });
58
+
59
+ phase("Scope");
60
+ const scope = await agent(
61
+ "Establish the scope for an advisory-only refactor scout. Do not edit files.\n" +
62
+ `Target / focus (verbatim): ${target}\n\n` +
63
+ "Inspect repository structure, the target path or module, and relevant AGENTS.md / project docs conventions. " +
64
+ "Return the concrete files that should be considered, a short summary, and any conventions that affect refactor advice. " +
65
+ `This workflow will fan out across ${REFACTOR_LENSES.length} lenses with up to ${PER_LENS} candidates per lens. Structured output only.`,
66
+ { phase: "Scope", label: "scope", tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS, profile: "medium", schema: ScopeSchema },
67
+ );
68
+
69
+ if (!scope || scope.files.length === 0) {
70
+ return finishAdvisoryReport(emptyAdvisoryReport(
71
+ "No files were identified for refactor scouting.",
72
+ ["Provide a target path, module, or subsystem to scout for refactor opportunities."],
73
+ makeStats(0, 0),
74
+ ), []);
75
+ }
76
+
77
+ fileCount = scope.files.length;
78
+ progress({ type: "counter", key: "files", label: "files", value: fileCount });
79
+ progress({ type: "summary", key: "files", value: scope.files.join(", ") });
80
+ log(`${scope.files.length} files scoped for refactor scouting`);
81
+
82
+ const scopeBlock =
83
+ `## Target\n${scope.target}\n\n## Files in scope\n${scope.files.map((file) => `- ${file}`).join("\n")}\n\n` +
84
+ `## Summary\n${scope.summary}\n\n## Conventions\n${scope.conventions ?? "(none noted)"}\n` +
85
+ (args.trim() ? `\n## User instructions (verbatim)\n${args.trim()}\n` : "");
86
+
87
+ const pipelineResult = await runLensVerificationPipeline({
88
+ api,
89
+ lenses: REFACTOR_LENSES,
90
+ perLens: PER_LENS,
91
+ finderPrompt: (lens) =>
92
+ `## Refactor-scout finder — ${lens.label}\n\n${scopeBlock}\n` +
93
+ "This workflow is advisory-only: do not edit files and do not propose broad rewrites.\n" +
94
+ `Scout through ONLY this lens:\n${lens.text}\n\n` +
95
+ `Surface up to ${PER_LENS} candidates. Use category exactly "${lens.category}". ` +
96
+ "Each candidate must include a one-line summary, locations, impact on maintainability or future correctness, and an optional safe first recommendation. " +
97
+ "Only include opportunities where a small, reviewable first step is plausible. Structured output only.",
98
+ verifierPrompt: (candidate) =>
99
+ `## Refactor-scout verifier\n\n${scopeBlock}\n## Candidate\n` +
100
+ `Location: ${formatLocation(candidate)}\nCategory: ${candidate.category}\nSummary: ${candidate.summary}\nImpact: ${candidate.impact}\n` +
101
+ `Recommendation: ${candidate.recommendation ?? "(none supplied)"}\n\n` +
102
+ "Read the relevant files and return CONFIRMED, PLAUSIBLE, NOT_SUBSTANTIATED, or REFUTED. " +
103
+ "Default toward REFUTED if the opportunity is generic, too broad, not evidenced by code, or lacks a safe first step. " +
104
+ "Evidence must quote or cite code. Structured output only.",
105
+ });
106
+ rawCandidateCount += pipelineResult.rawCandidates;
107
+ droppedCandidateCount += pipelineResult.dropped;
108
+ refutedCandidateCount += pipelineResult.refuted;
109
+ const { coverage } = pipelineResult;
110
+ const verified = await challengeFindings(api, pipelineResult.verified, scopeBlock, challengeConfig.options, coverage);
111
+ const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
112
+ const stats = makeStats(verified.length, surviving.length);
113
+ publishVerifiedKeptProgress({ progress, log }, verified.length, surviving.length);
114
+
115
+ if (surviving.length === 0) {
116
+ return finishAdvisoryReport(emptyAdvisoryReport("No refactor opportunities survived verification.", ["Leave the scoped code unchanged unless a human reviewer has additional context."], stats), coverage, verified);
117
+ }
118
+
119
+ const ranked = [...surviving].sort((a, b) => rank(a) - rank(b));
120
+ const block = ranked
121
+ .map(
122
+ (finding, index) =>
123
+ `### [${index}] IDs: ${finding.sourceCandidateIds.join(", ")} ${formatLocation(finding)} (${finding.verdict}, ${finding.category})\n` +
124
+ `${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}\nSafe first step: ${finding.recommendation ?? "(none supplied)"}`,
125
+ )
126
+ .join("\n\n");
127
+
128
+ const resolved = await synthesizeAdvisoryReport(api,
129
+ `## Synthesis: final refactor-scout report\n\n${ranked.length} opportunities survived independent verification.\n\n${block}\n\n` +
130
+ "Merge findings with the same root cause and rank highest leverage / lowest risk first. " +
131
+ "Select findings by ID. " +
132
+ "Severity is maintenance or future-correctness impact. " +
133
+ "Recommendations must be safe first refactor steps, not rewrites. Include concrete nextSteps for the host developer. Structured output only.",
134
+ ranked, coverage,
135
+ );
136
+ return finishAdvisoryReport({ ...resolved, stats: { ...stats, kept: resolved.findings.length } }, coverage, verified);
137
+ }
138
+
139
+ function rank(finding: AdvisoryVerified): number {
140
+ const verdictRank = finding.verdict === "CONFIRMED" ? 0 : 1;
141
+ const categoryRank = finding.category === "dead-code" || finding.category === "conventions" ? 2 : 0;
142
+ return verdictRank + categoryRank;
143
+ }
@@ -0,0 +1,169 @@
1
+ import { compactResults } from "../runtime/concurrency.ts";
2
+ import {
3
+ MAX_RESEARCH_LANES,
4
+ ResearchLaneResultSchema,
5
+ ResearchPlanSchema,
6
+ ResearchReportSchema,
7
+ ResearchVerificationSchema,
8
+ type ResearchClaimCandidate,
9
+ type ResearchReport,
10
+ } from "../runtime/research-contract.ts";
11
+ import {
12
+ buildClaimCandidates,
13
+ fallbackResearchReport,
14
+ normalizeResearchLanes,
15
+ sanitizeLaneResults,
16
+ sanitizeResearchReport,
17
+ sanitizeVerification,
18
+ unavailableResearchReport,
19
+ unavailableVerification,
20
+ } from "../runtime/research-evidence.ts";
21
+ import { WorkflowToolHintUnavailableError } from "../runtime/tool-capabilities.ts";
22
+ import type { WorkflowApi, WorkflowMeta } from "../runtime/types.ts";
23
+
24
+ export const meta: WorkflowMeta = {
25
+ name: "research",
26
+ description: "Source-grounded external research: decompose → gather direct-page evidence → independently verify claims → cited synthesis.",
27
+ phases: [{ title: "Plan" }, { title: "Gather" }, { title: "Verify" }, { title: "Synthesize" }],
28
+ };
29
+
30
+ const EXTERNAL_TOOLS: string[] = [];
31
+ const EXTERNAL_TOOL_HINTS = ["external-search"] as const;
32
+
33
+ export default async function run(api: WorkflowApi): Promise<ResearchReport> {
34
+ const { agent, parallel, phase, log, progress, args } = api;
35
+ const question = args.trim();
36
+ if (!question) return unavailableResearchReport("empty-question");
37
+
38
+ phase("Plan");
39
+ let plan;
40
+ try {
41
+ plan = await agent(
42
+ `Plan bounded, source-grounded research for the user's question and any scope constraints embedded in it.\n\n` +
43
+ `Question and constraints (verbatim):\n${question}\n\n` +
44
+ `Create at most ${MAX_RESEARCH_LANES} non-overlapping lanes. Each lane needs a stable short id, title, objective, and 1-4 concrete search queries. ` +
45
+ "Separate primary-source discovery, current status, counterevidence, or jurisdiction/timeframe only when relevant. " +
46
+ "Do not claim exhaustive coverage. Structured output only.",
47
+ {
48
+ phase: "Plan",
49
+ label: "plan",
50
+ tools: EXTERNAL_TOOLS,
51
+ toolHints: EXTERNAL_TOOL_HINTS,
52
+ requireToolHints: true,
53
+ profile: "medium",
54
+ resume: "off",
55
+ schema: ResearchPlanSchema,
56
+ },
57
+ );
58
+ } catch (error) {
59
+ if (error instanceof WorkflowToolHintUnavailableError) return unavailableResearchReport("missing-capability");
60
+ throw error;
61
+ }
62
+
63
+ if (!plan) return unavailableResearchReport("no-evidence");
64
+ const lanes = normalizeResearchLanes(plan);
65
+ if (lanes.length === 0) return unavailableResearchReport("no-evidence");
66
+ progress({ type: "counter", key: "research.lanes", label: "research lanes", value: lanes.length });
67
+ progress({ type: "summary", key: "research.question", value: question });
68
+
69
+ phase("Gather");
70
+ const gathered = compactResults(
71
+ await parallel(
72
+ lanes.map((lane) => async () => {
73
+ const result = await agent(
74
+ `Research one bounded lane using only installed external web-search, browsing, or URL-extraction tools.\n\n` +
75
+ `Question: ${question}\n` +
76
+ `Scope constraints: ${plan.scopeConstraints.join("; ") || "(none)"}\n` +
77
+ `Lane id: ${lane.id}\nLane: ${lane.title}\nObjective: ${lane.objective}\n` +
78
+ `Queries:\n${lane.queries.map((query) => `- ${query}`).join("\n")}\n\n` +
79
+ "Open the supporting pages instead of citing a search-results page. Prefer primary and authoritative sources; use independent sources when useful. " +
80
+ "Return concrete claims with importance, whether each page supports or conflicts with the claim, a short passage or precise paraphrase, and the exact page title and URL. " +
81
+ "State evidence gaps. Never invent a URL or claim that the opened page does not support. Structured output only.",
82
+ {
83
+ phase: "Gather",
84
+ label: `gather:${lane.id}`,
85
+ tools: EXTERNAL_TOOLS,
86
+ toolHints: EXTERNAL_TOOL_HINTS,
87
+ requireToolHints: true,
88
+ profile: "small",
89
+ resume: "off",
90
+ schema: ResearchLaneResultSchema,
91
+ },
92
+ );
93
+ if (!result) return null;
94
+ progress({ type: "counter_delta", key: "research.evidence", label: "evidence items", delta: result.evidence.length });
95
+ progress({
96
+ type: "lane_item",
97
+ lane: "Research lanes",
98
+ title: lane.title,
99
+ subtitle: `${result.evidence.length} evidence item(s)`,
100
+ status: result.evidence.length > 0 ? "success" : "warning",
101
+ details: result.gaps.join("; ") || lane.objective,
102
+ });
103
+ return result;
104
+ }),
105
+ ),
106
+ );
107
+
108
+ const laneResults = sanitizeLaneResults(gathered);
109
+ const candidates = buildClaimCandidates(laneResults);
110
+ if (candidates.length === 0) return unavailableResearchReport("no-evidence");
111
+ progress({ type: "counter", key: "research.claims", label: "claims to verify", value: candidates.length });
112
+ log(`${candidates.length} bounded claim(s) selected for independent verification`);
113
+
114
+ phase("Verify");
115
+ const verificationResults = await parallel(
116
+ candidates.map((candidate, index) => async () => {
117
+ const result = await agent(verificationPrompt(question, candidate), {
118
+ phase: "Verify",
119
+ label: `verify:${index + 1}`,
120
+ tools: EXTERNAL_TOOLS,
121
+ toolHints: EXTERNAL_TOOL_HINTS,
122
+ requireToolHints: true,
123
+ profile: "medium",
124
+ resume: "off",
125
+ schema: ResearchVerificationSchema,
126
+ });
127
+ return result ? sanitizeVerification(result, candidate) : null;
128
+ }),
129
+ );
130
+ const verifications = verificationResults.map((result, index) => result ?? unavailableVerification(candidates[index]!));
131
+ for (const verification of verifications) {
132
+ progress({ type: "counter_delta", key: `research.${verification.verdict.toLowerCase()}`, label: verification.verdict.toLowerCase(), delta: 1 });
133
+ }
134
+ const synthesisInputs = verifications.filter((verification) => verification.verdict !== "REJECTED");
135
+
136
+ phase("Synthesize");
137
+ const synthesis = await agent(
138
+ `Answer the research question using only the independently verified handoff below.\n\n` +
139
+ `Question: ${question}\nScope constraints: ${plan.scopeConstraints.join("; ") || "(none)"}\n\n` +
140
+ `Verified claims JSON:\n${JSON.stringify(synthesisInputs)}\n\n` +
141
+ "Keep SUPPORTED claims, CONFLICTED evidence, UNCERTAIN claims, and model INFERENCE in their separate fields. Exclude REJECTED claims. " +
142
+ "Copy each verified claim string exactly so its citations remain bound to that claim during validation. " +
143
+ "Every supported or conflicting claim must cite exact title/URL objects from its verification; do not add URLs, cite search-results pages, or turn inference into fact. " +
144
+ "Answer concisely, disclose limited coverage, and provide useful next steps. Structured output only.",
145
+ {
146
+ phase: "Synthesize",
147
+ label: "synthesize",
148
+ tools: [],
149
+ profile: "medium",
150
+ resume: "off",
151
+ schema: ResearchReportSchema,
152
+ },
153
+ );
154
+
155
+ return sanitizeResearchReport(
156
+ synthesis ?? fallbackResearchReport(synthesisInputs, "The model did not return a structured synthesis."),
157
+ synthesisInputs,
158
+ );
159
+ }
160
+
161
+ function verificationPrompt(question: string, candidate: ResearchClaimCandidate): string {
162
+ return `Independently verify one important research claim using installed external web-search, browsing, or URL-extraction tools.\n\n` +
163
+ `Question: ${question}\nClaim: ${candidate.claim}\nImportance: ${candidate.importance}\n\n` +
164
+ `Gather-stage evidence (context only; do not treat it as verified):\n${JSON.stringify(candidate.evidence)}\n\n` +
165
+ "Search independently and open direct supporting pages. Prefer primary/authoritative sources and actively look for credible counterevidence. " +
166
+ "Return SUPPORTED only when direct pages substantiate the claim, CONFLICTED when credible sources disagree, UNCERTAIN when evidence is insufficient, " +
167
+ "INFERENCE when the conclusion is reasoned rather than directly stated, or REJECTED when evidence refutes it. " +
168
+ "Include exact page titles and direct HTTP(S) URLs, never search-results URLs. Structured output only.";
169
+ }
@@ -2,15 +2,7 @@ import { agentPath, readJson, writeJson } from "../core/store.ts";
2
2
  import { isClaudeAccount, type UsageRow } from "../core/quota/pool.ts";
3
3
  import { fetchAll } from "../core/quota/usage-source.ts";
4
4
 
5
- /**
6
- * The single owner of subscription usage state.
7
- *
8
- * Previously the only poller lived inside the footer extension and was gated on
9
- * `ctx.hasUI`, so `list_models` silently read empty rows whenever the footer was
10
- * hidden or the session was headless (including every workflow subagent).
11
- * Consumers now call `ensureFresh()` for on-demand data and `subscribe()` for
12
- * push updates; only one poll is ever in flight regardless of consumer count.
13
- */
5
+ /** Shared subscription-usage cache and poller. */
14
6
 
15
7
  export const REFRESH_MS = 5 * 60 * 1000;
16
8
  const MIN_INTERVAL_MS = 90_000;
@@ -24,11 +16,7 @@ export interface UsageState {
24
16
  loading: boolean;
25
17
  accounts: number;
26
18
  codexPlan?: string;
27
- /**
28
- * Last time each account group's quota was observed to drop. This is the only
29
- * available proxy for "recently used": providers expose remaining quota but
30
- * never report which account served a request.
31
- */
19
+ /** Last observed quota drop for each account group. */
32
20
  lastUsedAt?: Record<string, number>;
33
21
  }
34
22
 
@@ -77,10 +65,7 @@ function emit(): void {
77
65
  }
78
66
  }
79
67
 
80
- /**
81
- * Stamps an account as recently used when its headline window falls. A rise
82
- * (quota reset) or an unchanged figure is not a usage signal.
83
- */
68
+ /** Records use when an account's headline quota falls. */
84
69
  function recordUsageDrops(fresh: UsageRow[]): void {
85
70
  const previous = new Map(
86
71
  state.rows.filter((row) => row.label === "5h").map((row) => [row.group, row.remaining]),
@@ -98,10 +83,7 @@ export function usageState(): UsageState {
98
83
  return state;
99
84
  }
100
85
 
101
- /**
102
- * Account groups ordered by most recent observed use, capped at `limit`.
103
- * Accounts never seen in use fall back to alphabetical, so the list is stable.
104
- */
86
+ /** Returns recently used account groups with stable fallback ordering. */
105
87
  export function recentAccounts(limit: number): string[] {
106
88
  const stamps = state.lastUsedAt ?? {};
107
89
  const groups = [...new Set(state.rows.filter(isClaudeAccount).map((row) => row.group))];
@@ -116,10 +98,7 @@ export function subscribe(listener: () => void): () => void {
116
98
  return () => listeners.delete(listener);
117
99
  }
118
100
 
119
- /**
120
- * The endpoints are rate limited, so results are cached and refreshes are
121
- * throttled. Failures keep the previous figures on screen.
122
- */
101
+ /** Refreshes cached usage without overlapping requests. */
123
102
  export async function refreshUsage(ctx: any, force = false): Promise<void> {
124
103
  if (inFlight) return inFlight;
125
104
  const now = Date.now();
@@ -160,10 +139,7 @@ export async function refreshUsage(ctx: any, force = false): Promise<void> {
160
139
  return inFlight;
161
140
  }
162
141
 
163
- /**
164
- * Guarantees usable data for a caller that does not own the poll loop.
165
- * This is what makes `list_models` correct in headless sessions.
166
- */
142
+ /** Refreshes stale data for UI and headless callers. */
167
143
  export async function ensureFresh(ctx: any): Promise<UsageState> {
168
144
  const stale = !state.updatedAt || Date.now() - state.updatedAt > REFRESH_MS;
169
145
  if (stale) await refreshUsage(ctx);
@@ -120,8 +120,7 @@ export function renderUsageLines(state: UsageState, theme: any, width: number, m
120
120
  const status = availability.ready
121
121
  ? `${availability.ready}/${availability.total} ready`
122
122
  : availability.unknown ? "unknown/stale" : "exhausted";
123
- const partial = claude.some((cell) => cell.partial);
124
- const claudeTitle = `Claude Σ${state.accounts} · ${status}${partial ? " · partial" : ""}`;
123
+ const claudeTitle = `Claude Σ${state.accounts} · ${status}`;
125
124
  const codexTitle = state.codexPlan ? `Codex · ${state.codexPlan}` : "Codex";
126
125
  const lines = [` ${theme.fg("accent", claudeTitle.padEnd(cellWidth))}${" ".repeat(gap)}${theme.fg("accent", codexTitle)}`];
127
126
 
@@ -1,129 +0,0 @@
1
- import { generatePkce, generateState, parseCallback } from "../oauth/pkce.ts";
2
-
3
- /**
4
- * Codex OAuth (ChatGPT account login).
5
- *
6
- * The client id is the public one the Codex CLI itself uses; it is not a
7
- * secret and the flow is PKCE precisely so that no secret is required. The
8
- * redirect is a loopback URL, which is what lets a CLI complete the flow.
9
- */
10
-
11
- const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
12
- const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
13
- const TOKEN_URL = "https://auth.openai.com/oauth/token";
14
- const REDIRECT_URI = "http://localhost:1455/auth/callback";
15
- const SCOPE = "openid profile email offline_access";
16
-
17
- export interface CodexAuthStart {
18
- url: string;
19
- verifier: string;
20
- state: string;
21
- redirectUri: string;
22
- }
23
-
24
- export interface CodexTokens {
25
- access: string;
26
- refresh: string;
27
- expires: number;
28
- }
29
-
30
- /** Builds the authorization URL and the PKCE material needed to finish. */
31
- export async function authorizeCodex(): Promise<CodexAuthStart> {
32
- const pkce = await generatePkce();
33
- const state = generateState();
34
-
35
- const url = new URL(AUTHORIZE_URL);
36
- url.searchParams.set("response_type", "code");
37
- url.searchParams.set("client_id", CLIENT_ID);
38
- url.searchParams.set("redirect_uri", REDIRECT_URI);
39
- url.searchParams.set("scope", SCOPE);
40
- url.searchParams.set("code_challenge", pkce.challenge);
41
- url.searchParams.set("code_challenge_method", "S256");
42
- url.searchParams.set("state", state);
43
- // Forces the account chooser, which is the entire point when adding a
44
- // SECOND account: without it the browser silently reuses the existing
45
- // session and you get a duplicate of the account you already have.
46
- url.searchParams.set("prompt", "login");
47
-
48
- return { url: url.toString(), verifier: pkce.verifier, state, redirectUri: REDIRECT_URI };
49
- }
50
-
51
- /** Exchanges the callback URL (or a bare code) for tokens. */
52
- export async function exchangeCodex(
53
- callback: string,
54
- verifier: string,
55
- redirectUri: string,
56
- expectedState?: string,
57
- ): Promise<CodexTokens> {
58
- const parsed = parseCallback(callback);
59
- const code = parsed?.code ?? callback.trim();
60
- if (!code) throw new Error("No authorization code found in the callback.");
61
- if (expectedState && parsed?.state && parsed.state !== expectedState) {
62
- throw new Error("OAuth state mismatch; the callback does not belong to this login attempt.");
63
- }
64
-
65
- const response = await fetch(TOKEN_URL, {
66
- method: "POST",
67
- headers: { "content-type": "application/json" },
68
- body: JSON.stringify({
69
- grant_type: "authorization_code",
70
- client_id: CLIENT_ID,
71
- code,
72
- redirect_uri: redirectUri,
73
- code_verifier: verifier,
74
- }),
75
- });
76
-
77
- if (!response.ok) {
78
- throw new Error(`Codex token exchange failed (${response.status}): ${(await response.text()).slice(0, 200)}`);
79
- }
80
-
81
- const body: any = await response.json();
82
- if (!body?.access_token) throw new Error("Codex token exchange returned no access token.");
83
- return {
84
- access: body.access_token,
85
- refresh: body.refresh_token,
86
- expires: Date.now() + Number(body.expires_in ?? 3600) * 1000,
87
- };
88
- }
89
-
90
- /** Refreshes an expiring token. */
91
- export async function refreshCodexToken(refresh: string): Promise<CodexTokens> {
92
- const response = await fetch(TOKEN_URL, {
93
- method: "POST",
94
- headers: { "content-type": "application/json" },
95
- body: JSON.stringify({
96
- grant_type: "refresh_token",
97
- client_id: CLIENT_ID,
98
- refresh_token: refresh,
99
- scope: SCOPE,
100
- }),
101
- });
102
-
103
- if (!response.ok) {
104
- throw new Error(`Codex token refresh failed (${response.status}).`);
105
- }
106
-
107
- const body: any = await response.json();
108
- if (!body?.access_token) throw new Error("Codex token refresh returned no access token.");
109
- return {
110
- // OpenAI may omit a rotated refresh token; keeping the old one is correct.
111
- access: body.access_token,
112
- refresh: body.refresh_token ?? refresh,
113
- expires: Date.now() + Number(body.expires_in ?? 3600) * 1000,
114
- };
115
- }
116
-
117
- /** A token good for at least a minute, refreshing and persisting if needed. */
118
- export async function ensureCodexToken(
119
- account: { access?: string; refresh?: string; expires?: number },
120
- persist: (tokens: CodexTokens) => void,
121
- ): Promise<string | undefined> {
122
- if (account.access && typeof account.expires === "number" && Date.now() + 60_000 < account.expires) {
123
- return account.access;
124
- }
125
- if (!account.refresh) return account.access;
126
- const tokens = await refreshCodexToken(account.refresh);
127
- persist(tokens);
128
- return tokens.access;
129
- }