@companion-ai/feynman 0.3.4 → 0.3.5
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.
- package/.feynman/runtime-workspace.tgz +0 -0
- package/RELEASES.md +13 -0
- package/dist/model/catalog.js +1 -1
- package/dist/pi/package-ops.js +7 -7
- package/dist/rank/paper-rank.js +181 -19
- package/dist/research/contracts.js +92 -0
- package/dist/research/plugin-manifest.js +131 -0
- package/package.json +5 -5
- package/scripts/lib/pi-tui-patch.mjs +81 -3
- package/scripts/prepare-runtime-workspace.mjs +1 -1
|
Binary file
|
package/RELEASES.md
CHANGED
|
@@ -4,6 +4,19 @@ This file is the public release history for Feynman. Keep entries user-facing: w
|
|
|
4
4
|
|
|
5
5
|
GitHub release notes are generated from the matching `## vX.Y.Z` section in this file.
|
|
6
6
|
|
|
7
|
+
## v0.3.5 - 2026-06-28
|
|
8
|
+
|
|
9
|
+
### Pi Runtime
|
|
10
|
+
|
|
11
|
+
- Refreshed the bundled Pi runtime from `0.79.10` to `0.80.2` across all four packages (`pi-coding-agent`, `pi-agent-core`, `pi-ai`, `pi-tui`). This restores the `@earendil-works/pi-ai/compat` entrypoint and loader aliases used by optional packages such as `pi-web-access`, fixing the extension-load failure reported in #183.
|
|
12
|
+
- Feynman's package installer now derives legacy `@mariozechner/*` alias versions from the current canonical `@earendil-works/*` runtime packages first, so stale legacy package roots cannot seed old Pi peer versions during `feynman update`.
|
|
13
|
+
- Updated the Pi TUI patcher for the current upstream overflow-check layout so overwide rendered lines are clipped instead of crashing the session renderer.
|
|
14
|
+
|
|
15
|
+
### Validation
|
|
16
|
+
|
|
17
|
+
- Added regression coverage for the current Pi TUI overflow block, the `@earendil-works/pi-ai/compat` release-note boundary, and legacy Pi alias derivation from current runtime metadata.
|
|
18
|
+
- Rebuilt and inspected the vendored runtime workspace so the packaged archive includes Pi `0.80.2`, `@earendil-works/pi-ai/dist/compat.js`, and the current/legacy `/compat` extension-loader aliases.
|
|
19
|
+
|
|
7
20
|
## v0.3.4 - 2026-06-12
|
|
8
21
|
|
|
9
22
|
### Research
|
package/dist/model/catalog.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { getEnvApiKey } from "@earendil-works/pi-ai";
|
|
2
|
+
import { getEnvApiKey } from "@earendil-works/pi-ai/compat";
|
|
3
3
|
import { createModelRegistry } from "./registry.js";
|
|
4
4
|
const PRO_CLASS_MODEL_PATTERN = /(?:^|[-_.:/])pro(?:$|[-_.:/])/i;
|
|
5
5
|
const PROVIDER_LABELS = {
|
package/dist/pi/package-ops.js
CHANGED
|
@@ -17,7 +17,7 @@ const FILTERED_INSTALL_OUTPUT_PATTERNS = [
|
|
|
17
17
|
/^run `npm fund` for details$/i,
|
|
18
18
|
];
|
|
19
19
|
const APP_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
20
|
-
const PI_RUNTIME_FALLBACK_VERSION = "0.
|
|
20
|
+
const PI_RUNTIME_FALLBACK_VERSION = "0.80.2";
|
|
21
21
|
const LEGACY_PI_RUNTIME_PACKAGE_ALIASES = {
|
|
22
22
|
"@mariozechner/pi-agent-core": "@earendil-works/pi-agent-core",
|
|
23
23
|
"@mariozechner/pi-ai": "@earendil-works/pi-ai",
|
|
@@ -114,6 +114,12 @@ function isPiRuntimePackageName(packageName) {
|
|
|
114
114
|
return packageName.startsWith("pi-") || packageName.includes("/pi-");
|
|
115
115
|
}
|
|
116
116
|
function resolveRuntimePeerSpec(packageName) {
|
|
117
|
+
const aliasTarget = LEGACY_PI_RUNTIME_PACKAGE_ALIASES[packageName];
|
|
118
|
+
if (aliasTarget) {
|
|
119
|
+
const targetSpec = resolveRuntimePeerSpec(aliasTarget);
|
|
120
|
+
const targetVersion = targetSpec?.match(/@(\d+\.\d+\.\d+)$/)?.[1] ?? PI_RUNTIME_FALLBACK_VERSION;
|
|
121
|
+
return `${packageName}@npm:${aliasTarget}@${targetVersion}`;
|
|
122
|
+
}
|
|
117
123
|
for (const packageRoot of [
|
|
118
124
|
resolve(APP_ROOT, "node_modules", packageName),
|
|
119
125
|
resolve(APP_ROOT, ".feynman", "npm", "node_modules", packageName),
|
|
@@ -133,12 +139,6 @@ function resolveRuntimePeerSpec(packageName) {
|
|
|
133
139
|
continue;
|
|
134
140
|
}
|
|
135
141
|
}
|
|
136
|
-
const aliasTarget = LEGACY_PI_RUNTIME_PACKAGE_ALIASES[packageName];
|
|
137
|
-
if (aliasTarget) {
|
|
138
|
-
const targetSpec = resolveRuntimePeerSpec(aliasTarget);
|
|
139
|
-
const targetVersion = targetSpec?.match(/@(\d+\.\d+\.\d+)$/)?.[1] ?? PI_RUNTIME_FALLBACK_VERSION;
|
|
140
|
-
return `${packageName}@npm:${aliasTarget}@${targetVersion}`;
|
|
141
|
-
}
|
|
142
142
|
return FALLBACK_RUNTIME_PEER_SPECS[packageName];
|
|
143
143
|
}
|
|
144
144
|
function withRuntimePeerSpecs(specs) {
|
package/dist/rank/paper-rank.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { XMLParser } from "fast-xml-parser";
|
|
5
|
+
import { buildResearchRunId, createResearchArtifact, validateResearchRun } from "../research/contracts.js";
|
|
5
6
|
export const DEFAULT_RANK_LIMIT = 25;
|
|
6
7
|
export const MAX_RANK_LIMIT = 100;
|
|
7
8
|
export const DEFAULT_FULL_TEXT_TOP = 0;
|
|
@@ -3685,6 +3686,7 @@ function renderPaperAccessReport(input) {
|
|
|
3685
3686
|
}
|
|
3686
3687
|
function writePaperRankArtifacts(input) {
|
|
3687
3688
|
mkdirSync(input.outputDir, { recursive: true });
|
|
3689
|
+
const researchRunPath = resolve(input.outputDir, `${input.slug}-research-run.json`);
|
|
3688
3690
|
const reportPath = resolve(input.outputDir, `${input.slug}-paper-rank.md`);
|
|
3689
3691
|
const papersPath = resolve(input.outputDir, `${input.slug}-papers.jsonl`);
|
|
3690
3692
|
const scoresPath = resolve(input.outputDir, `${input.slug}-scores.jsonl`);
|
|
@@ -3704,6 +3706,33 @@ function writePaperRankArtifacts(input) {
|
|
|
3704
3706
|
const critiquePath = input.critiques.length > 0 ? resolve(input.outputDir, `${input.slug}-critique.md`) : undefined;
|
|
3705
3707
|
const modelSynthesisPath = input.synthesis.status === "generated" && input.synthesis.text ? resolve(input.outputDir, `${input.slug}-model-synthesis.md`) : undefined;
|
|
3706
3708
|
const provenancePath = resolve(input.outputDir, `${input.slug}-rank.provenance.md`);
|
|
3709
|
+
const artifacts = {
|
|
3710
|
+
researchRunPath,
|
|
3711
|
+
reportPath,
|
|
3712
|
+
papersPath,
|
|
3713
|
+
scoresPath,
|
|
3714
|
+
scoreAuditPath,
|
|
3715
|
+
sensitivityPath,
|
|
3716
|
+
graphPath,
|
|
3717
|
+
graphExplorerPath,
|
|
3718
|
+
fieldMapPath,
|
|
3719
|
+
provenancePath,
|
|
3720
|
+
...(calibrationPath ? { calibrationPath } : {}),
|
|
3721
|
+
...(calibrationTemplatePath ? { calibrationTemplatePath } : {}),
|
|
3722
|
+
...(calibrationGuidePath ? { calibrationGuidePath } : {}),
|
|
3723
|
+
...(reproductionLedgerPath ? { reproductionLedgerPath } : {}),
|
|
3724
|
+
...(reproductionTemplatePath ? { reproductionTemplatePath } : {}),
|
|
3725
|
+
...(replicationPlanPath ? { replicationPlanPath } : {}),
|
|
3726
|
+
...(synthesisPacketPath ? { synthesisPacketPath } : {}),
|
|
3727
|
+
...(synthesisPromptPath ? { synthesisPromptPath } : {}),
|
|
3728
|
+
...(critiquePath ? { critiquePath } : {}),
|
|
3729
|
+
...(modelSynthesisPath ? { modelSynthesisPath } : {}),
|
|
3730
|
+
};
|
|
3731
|
+
const researchRun = buildPaperRankResearchRun(input, artifacts);
|
|
3732
|
+
const researchRunValidation = validateResearchRun(researchRun);
|
|
3733
|
+
if (!researchRunValidation.valid) {
|
|
3734
|
+
throw new Error(`Invalid PaperRank ResearchRun manifest: ${researchRunValidation.errors.join("; ")}`);
|
|
3735
|
+
}
|
|
3707
3736
|
writeFileSync(reportPath, renderRankReport(input), "utf8");
|
|
3708
3737
|
writeFileSync(papersPath, input.papers.map((paper) => JSON.stringify(serializePaperRecord(paper))).join("\n") + "\n", "utf8");
|
|
3709
3738
|
writeFileSync(scoresPath, input.scores.map((score) => JSON.stringify(score)).join("\n") + "\n", "utf8");
|
|
@@ -3733,26 +3762,159 @@ function writePaperRankArtifacts(input) {
|
|
|
3733
3762
|
if (modelSynthesisPath && input.synthesis.text)
|
|
3734
3763
|
writeFileSync(modelSynthesisPath, renderModelSynthesisReport(input), "utf8");
|
|
3735
3764
|
writeFileSync(provenancePath, renderRankProvenance(input), "utf8");
|
|
3765
|
+
writeFileSync(researchRunPath, JSON.stringify(researchRun, null, 2) + "\n", "utf8");
|
|
3766
|
+
return artifacts;
|
|
3767
|
+
}
|
|
3768
|
+
function definedArtifact(artifact) {
|
|
3769
|
+
return artifact !== undefined;
|
|
3770
|
+
}
|
|
3771
|
+
function buildPaperRankResearchRun(input, artifacts) {
|
|
3772
|
+
const papersById = new Map(input.papers.map((paper) => [paper.paperId, paper]));
|
|
3773
|
+
const rolesByPaper = new Map(input.fieldMap.paperRoles.map((role) => [role.paperId, role.roles]));
|
|
3774
|
+
const reproductionByPaper = new Map(input.reproduction.papers.map((paper) => [paper.paperId, paper]));
|
|
3775
|
+
const researchArtifacts = [
|
|
3776
|
+
createResearchArtifact({ kind: "manifest", path: artifacts.researchRunPath, label: "ResearchRun manifest", role: "research_run_manifest", format: "application/json" }),
|
|
3777
|
+
createResearchArtifact({ kind: "report", path: artifacts.reportPath, label: "PaperRank ranked brief", role: "primary_ranked_brief", primary: true, format: "text/markdown" }),
|
|
3778
|
+
createResearchArtifact({ kind: "jsonl", path: artifacts.papersPath, label: "normalized papers", role: "paper_records", format: "application/jsonl" }),
|
|
3779
|
+
createResearchArtifact({ kind: "jsonl", path: artifacts.scoresPath, label: "component scores", role: "score_records", format: "application/jsonl" }),
|
|
3780
|
+
createResearchArtifact({ kind: "audit", path: artifacts.scoreAuditPath, label: "score audit", role: "score_explanation", format: "text/markdown" }),
|
|
3781
|
+
createResearchArtifact({ kind: "json", path: artifacts.sensitivityPath, label: "rank sensitivity", role: "weight_sensitivity", format: "application/json" }),
|
|
3782
|
+
createResearchArtifact({ kind: "graph", path: artifacts.graphPath, label: "citation graph", role: "citation_graph", format: "application/json" }),
|
|
3783
|
+
createResearchArtifact({ kind: "html", path: artifacts.graphExplorerPath, label: "graph explorer", role: "graph_visualizer", format: "text/html" }),
|
|
3784
|
+
createResearchArtifact({ kind: "json", path: artifacts.fieldMapPath, label: "field map", role: "field_structure", format: "application/json" }),
|
|
3785
|
+
createResearchArtifact({ kind: "provenance", path: artifacts.provenancePath, label: "rank provenance", role: "source_accounting", format: "text/markdown" }),
|
|
3786
|
+
createResearchArtifact({ kind: "json", path: artifacts.calibrationPath, label: "score calibration", role: "read_order_calibration", format: "application/json" }),
|
|
3787
|
+
createResearchArtifact({ kind: "template", path: artifacts.calibrationTemplatePath, label: "calibration template", role: "read_order_preference_template", format: "application/json" }),
|
|
3788
|
+
createResearchArtifact({ kind: "report", path: artifacts.calibrationGuidePath, label: "calibration guide", role: "read_order_calibration_instructions", format: "text/markdown" }),
|
|
3789
|
+
createResearchArtifact({ kind: "ledger", path: artifacts.reproductionLedgerPath, label: "reproduction ledger", role: "completed_reproduction_evidence", format: "application/json" }),
|
|
3790
|
+
createResearchArtifact({ kind: "template", path: artifacts.reproductionTemplatePath, label: "reproduction notes template", role: "reproduction_evidence_template", format: "application/json" }),
|
|
3791
|
+
createResearchArtifact({ kind: "plan", path: artifacts.replicationPlanPath, label: "replication plan", role: "reproduction_plan", format: "text/markdown" }),
|
|
3792
|
+
createResearchArtifact({ kind: "json", path: artifacts.synthesisPacketPath, label: "model synthesis packet", role: "bounded_model_handoff", format: "application/json" }),
|
|
3793
|
+
createResearchArtifact({ kind: "prompt", path: artifacts.synthesisPromptPath, label: "model synthesis prompt", role: "bounded_model_prompt", format: "text/markdown" }),
|
|
3794
|
+
createResearchArtifact({ kind: "report", path: artifacts.critiquePath, label: "research critique", role: "deterministic_research_critique", format: "text/markdown" }),
|
|
3795
|
+
createResearchArtifact({ kind: "model_output", path: artifacts.modelSynthesisPath, label: "model synthesis", role: "generated_synthesis", format: "text/markdown" }),
|
|
3796
|
+
].filter(definedArtifact);
|
|
3797
|
+
const fullTextAvailable = input.papers.filter((paper) => paper.fullTextStatus === "available").length;
|
|
3798
|
+
const fullTextAttempted = input.papers.filter((paper) => Boolean(paper.fullTextStatus)).length;
|
|
3799
|
+
const tools = [
|
|
3800
|
+
{
|
|
3801
|
+
id: input.source === "fixture" ? "fixture.openalex" : "openalex.works",
|
|
3802
|
+
kind: "source_adapter",
|
|
3803
|
+
label: input.source === "fixture" ? "OpenAlex fixture source" : "OpenAlex works search",
|
|
3804
|
+
status: "completed",
|
|
3805
|
+
outputArtifacts: [artifacts.papersPath],
|
|
3806
|
+
caveats: input.source === "fixture" ? ["Fixture source is deterministic test data, not live provider evidence."] : [],
|
|
3807
|
+
},
|
|
3808
|
+
{
|
|
3809
|
+
id: "feynman.paper_rank.scoring",
|
|
3810
|
+
kind: "rank_scorer",
|
|
3811
|
+
label: "PaperRank deterministic scoring",
|
|
3812
|
+
status: "completed",
|
|
3813
|
+
outputArtifacts: [artifacts.scoresPath, artifacts.scoreAuditPath],
|
|
3814
|
+
caveats: ["Methodology and reproducibility are screening signals; they are not completed claim validation."],
|
|
3815
|
+
},
|
|
3816
|
+
{
|
|
3817
|
+
id: "feynman.paper_rank.field_map",
|
|
3818
|
+
kind: "artifact_exporter",
|
|
3819
|
+
label: "Field map and citation graph artifacts",
|
|
3820
|
+
status: "completed",
|
|
3821
|
+
outputArtifacts: [artifacts.fieldMapPath, artifacts.graphPath, artifacts.graphExplorerPath],
|
|
3822
|
+
},
|
|
3823
|
+
{
|
|
3824
|
+
id: "feynman.paper_rank.full_text",
|
|
3825
|
+
kind: "access_resolver",
|
|
3826
|
+
label: "source-specific full-text enrichment",
|
|
3827
|
+
status: input.fullTextTop > 0 ? (fullTextAttempted > 0 ? "completed" : "partial") : "not_run",
|
|
3828
|
+
outputArtifacts: [artifacts.papersPath, artifacts.scoresPath],
|
|
3829
|
+
caveats: ["Raw full-text bodies are not written to PaperRank artifacts."],
|
|
3830
|
+
},
|
|
3831
|
+
...(input.synthesis.requested ? [{
|
|
3832
|
+
id: "feynman.paper_rank.model_synthesis",
|
|
3833
|
+
kind: "model",
|
|
3834
|
+
label: "bounded PaperRank model synthesis",
|
|
3835
|
+
status: input.synthesis.status === "generated" ? "completed" : input.synthesis.status === "failed" ? "failed" : "partial",
|
|
3836
|
+
outputArtifacts: [artifacts.synthesisPacketPath, artifacts.synthesisPromptPath, artifacts.modelSynthesisPath].filter((path) => Boolean(path)),
|
|
3837
|
+
caveats: ["Deterministic artifacts remain the audit trail; generated synthesis is a narrative handoff."],
|
|
3838
|
+
}] : []),
|
|
3839
|
+
];
|
|
3736
3840
|
return {
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3841
|
+
schemaVersion: "feynman.researchRun.v1",
|
|
3842
|
+
runId: buildResearchRunId({ workflow: "paper_rank", slug: input.slug, generatedAt: input.generatedAt }),
|
|
3843
|
+
workflow: "paper_rank",
|
|
3844
|
+
slug: input.slug,
|
|
3845
|
+
topic: input.topic,
|
|
3846
|
+
generatedAt: input.generatedAt,
|
|
3847
|
+
status: "completed",
|
|
3848
|
+
researchJobs: [
|
|
3849
|
+
"discovering_prior_art",
|
|
3850
|
+
"reading_paper_content",
|
|
3851
|
+
"ranking_evidence",
|
|
3852
|
+
"verifying_claims",
|
|
3853
|
+
"planning_reproduction",
|
|
3854
|
+
"synthesizing_artifacts",
|
|
3855
|
+
"visualizing_research_structure",
|
|
3856
|
+
"improving_research_loop",
|
|
3857
|
+
],
|
|
3858
|
+
sources: [
|
|
3859
|
+
{
|
|
3860
|
+
id: input.source,
|
|
3861
|
+
kind: input.source === "fixture" ? "fixture" : "paper_index",
|
|
3862
|
+
url: input.sourceUrl,
|
|
3863
|
+
fields: ["works", "metadata", "citations", "open_access", "abstracts"],
|
|
3864
|
+
},
|
|
3865
|
+
...(input.fullTextTop > 0 ? [{
|
|
3866
|
+
id: "source-specific-full-text",
|
|
3867
|
+
kind: "full_text",
|
|
3868
|
+
fields: ["fullTextStatus", "fullTextLength", "fullTextSections", "rubric"],
|
|
3869
|
+
}] : []),
|
|
3870
|
+
],
|
|
3871
|
+
papers: input.scores.map((score) => {
|
|
3872
|
+
const paper = papersById.get(score.paperId);
|
|
3873
|
+
const reproduction = reproductionByPaper.get(score.paperId);
|
|
3874
|
+
return {
|
|
3875
|
+
id: score.paperId,
|
|
3876
|
+
title: score.title,
|
|
3877
|
+
rank: score.rank,
|
|
3878
|
+
score: score.readFirstScore,
|
|
3879
|
+
...(score.year ? { year: score.year } : {}),
|
|
3880
|
+
...(paper?.doi ? { doi: paper.doi } : {}),
|
|
3881
|
+
...(paper?.arxivId ? { arxivId: paper.arxivId } : {}),
|
|
3882
|
+
...(paper?.pmid ? { pmid: paper.pmid } : {}),
|
|
3883
|
+
...(paper?.pmcid ? { pmcid: paper.pmcid } : {}),
|
|
3884
|
+
...(paper?.openAlexId ? { openAlexId: paper.openAlexId } : {}),
|
|
3885
|
+
...(paper?.urls[0]?.url ? { url: paper.urls[0].url } : {}),
|
|
3886
|
+
...(rolesByPaper.has(score.paperId) ? { roles: rolesByPaper.get(score.paperId) } : {}),
|
|
3887
|
+
verification: {
|
|
3888
|
+
state: reproduction?.status && reproduction.status !== "not_started" ? "partial" : "not_checked",
|
|
3889
|
+
summary: reproduction?.status && reproduction.status !== "not_started"
|
|
3890
|
+
? `External reproduction note recorded: ${reproduction.status}.`
|
|
3891
|
+
: "No completed reproduction note supplied for this run.",
|
|
3892
|
+
},
|
|
3893
|
+
};
|
|
3894
|
+
}),
|
|
3895
|
+
entities: [],
|
|
3896
|
+
tools,
|
|
3897
|
+
artifacts: researchArtifacts,
|
|
3898
|
+
nextActions: input.nextResearchActions.nextActions.slice(0, 20).map((action) => ({
|
|
3899
|
+
id: action.id,
|
|
3900
|
+
title: action.title,
|
|
3901
|
+
priority: action.priority,
|
|
3902
|
+
artifactPointers: action.artifactPointers,
|
|
3903
|
+
})),
|
|
3904
|
+
verification: {
|
|
3905
|
+
state: "partial",
|
|
3906
|
+
summary: `PaperRank ranked ${input.scores.length} seed papers, expanded ${input.citationExpansion.expandedPaperCount} citation-neighborhood papers, fetched full text for ${fullTextAvailable}/${input.fullTextTop} requested papers, and generated ${input.nextResearchActions.summary.actionCount} next research actions.`,
|
|
3907
|
+
caveats: [
|
|
3908
|
+
"PaperRank is read-order triage, not a completed scientific validation.",
|
|
3909
|
+
"Missing evidence means Feynman did not see it in fetched sources; it is not proof the paper lacks it.",
|
|
3910
|
+
"Raw full-text bodies are not stored in the ResearchRun manifest.",
|
|
3911
|
+
],
|
|
3912
|
+
},
|
|
3913
|
+
constraints: {
|
|
3914
|
+
rawFullTextStored: false,
|
|
3915
|
+
promptsStored: Boolean(artifacts.synthesisPromptPath),
|
|
3916
|
+
modelOutputsStored: Boolean(artifacts.modelSynthesisPath),
|
|
3917
|
+
},
|
|
3756
3918
|
};
|
|
3757
3919
|
}
|
|
3758
3920
|
function renderScoreAuditReport(input) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const researchJobValues = [
|
|
3
|
+
"discovering_prior_art",
|
|
4
|
+
"reading_paper_content",
|
|
5
|
+
"extracting_research_entities",
|
|
6
|
+
"ranking_evidence",
|
|
7
|
+
"verifying_claims",
|
|
8
|
+
"planning_reproduction",
|
|
9
|
+
"running_research_experiments",
|
|
10
|
+
"synthesizing_artifacts",
|
|
11
|
+
"visualizing_research_structure",
|
|
12
|
+
"improving_research_loop",
|
|
13
|
+
];
|
|
14
|
+
export const researchRunStatusValues = ["planned", "running", "completed", "partial", "blocked", "failed"];
|
|
15
|
+
export const verificationStateValues = ["not_checked", "inferred", "partial", "verified", "blocked", "failed"];
|
|
16
|
+
export const researchArtifactKindValues = [
|
|
17
|
+
"report",
|
|
18
|
+
"json",
|
|
19
|
+
"jsonl",
|
|
20
|
+
"graph",
|
|
21
|
+
"html",
|
|
22
|
+
"audit",
|
|
23
|
+
"provenance",
|
|
24
|
+
"template",
|
|
25
|
+
"prompt",
|
|
26
|
+
"model_output",
|
|
27
|
+
"ledger",
|
|
28
|
+
"plan",
|
|
29
|
+
"manifest",
|
|
30
|
+
];
|
|
31
|
+
export function buildResearchRunId(input) {
|
|
32
|
+
const digest = createHash("sha256")
|
|
33
|
+
.update(`${input.workflow}\0${input.slug}\0${input.generatedAt}`)
|
|
34
|
+
.digest("hex")
|
|
35
|
+
.slice(0, 16);
|
|
36
|
+
return `${input.workflow}:${input.slug}:${digest}`;
|
|
37
|
+
}
|
|
38
|
+
export function createResearchArtifact(input) {
|
|
39
|
+
if (!input.path)
|
|
40
|
+
return undefined;
|
|
41
|
+
return {
|
|
42
|
+
kind: input.kind,
|
|
43
|
+
path: input.path,
|
|
44
|
+
label: input.label,
|
|
45
|
+
role: input.role,
|
|
46
|
+
...(input.primary === undefined ? {} : { primary: input.primary }),
|
|
47
|
+
...(input.format ? { format: input.format } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function validateResearchRun(run) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
if (run.schemaVersion !== "feynman.researchRun.v1")
|
|
53
|
+
errors.push("schemaVersion must be feynman.researchRun.v1");
|
|
54
|
+
if (!run.runId.trim())
|
|
55
|
+
errors.push("runId is required");
|
|
56
|
+
if (!run.workflow.trim())
|
|
57
|
+
errors.push("workflow is required");
|
|
58
|
+
if (!run.slug.trim())
|
|
59
|
+
errors.push("slug is required");
|
|
60
|
+
if (!run.topic.trim())
|
|
61
|
+
errors.push("topic is required");
|
|
62
|
+
if (!Date.parse(run.generatedAt))
|
|
63
|
+
errors.push("generatedAt must be an ISO timestamp");
|
|
64
|
+
if (!researchRunStatusValues.includes(run.status))
|
|
65
|
+
errors.push(`status is unsupported: ${run.status}`);
|
|
66
|
+
if (run.researchJobs.length === 0)
|
|
67
|
+
errors.push("researchJobs must be non-empty");
|
|
68
|
+
for (const job of run.researchJobs) {
|
|
69
|
+
if (!researchJobValues.includes(job))
|
|
70
|
+
errors.push(`researchJobs contains unsupported value: ${job}`);
|
|
71
|
+
}
|
|
72
|
+
if (run.artifacts.length === 0)
|
|
73
|
+
errors.push("artifacts must be non-empty");
|
|
74
|
+
if (!run.artifacts.some((artifact) => artifact.primary))
|
|
75
|
+
errors.push("at least one artifact must be primary");
|
|
76
|
+
for (const artifact of run.artifacts) {
|
|
77
|
+
if (!researchArtifactKindValues.includes(artifact.kind))
|
|
78
|
+
errors.push(`artifact kind is unsupported: ${artifact.kind}`);
|
|
79
|
+
if (!artifact.path.trim())
|
|
80
|
+
errors.push(`artifact path is required for ${artifact.label}`);
|
|
81
|
+
if (!artifact.role.trim())
|
|
82
|
+
errors.push(`artifact role is required for ${artifact.label}`);
|
|
83
|
+
}
|
|
84
|
+
if (!verificationStateValues.includes(run.verification.state))
|
|
85
|
+
errors.push(`verification state is unsupported: ${run.verification.state}`);
|
|
86
|
+
if (run.constraints.rawFullTextStored)
|
|
87
|
+
errors.push("ResearchRun manifests must not mark rawFullTextStored true");
|
|
88
|
+
return {
|
|
89
|
+
valid: errors.length === 0,
|
|
90
|
+
errors,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { isAbsolute, normalize } from "node:path";
|
|
2
|
+
import { researchJobValues } from "./contracts.js";
|
|
3
|
+
export const feynmanPluginSlotValues = [
|
|
4
|
+
"source_adapters",
|
|
5
|
+
"access_resolvers",
|
|
6
|
+
"entity_extractors",
|
|
7
|
+
"rank_scorers",
|
|
8
|
+
"experiment_runners",
|
|
9
|
+
"artifact_exporters",
|
|
10
|
+
"visualizers",
|
|
11
|
+
"subagents",
|
|
12
|
+
"mcp_servers",
|
|
13
|
+
];
|
|
14
|
+
const knownTopLevelKeys = new Set(["manifest_version", "name", "version", "description", "research_jobs", "slots", "pi", "requires_env"]);
|
|
15
|
+
const piResourceKeys = new Set(["extensions", "skills", "prompts", "themes"]);
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function isSafePackageName(name) {
|
|
20
|
+
return /^[a-z0-9][a-z0-9._-]*$/i.test(name) && !name.includes("..") && !name.includes("/") && !name.includes("\\");
|
|
21
|
+
}
|
|
22
|
+
function isSafeRelativePath(path) {
|
|
23
|
+
if (!path.trim())
|
|
24
|
+
return false;
|
|
25
|
+
if (isAbsolute(path))
|
|
26
|
+
return false;
|
|
27
|
+
const normalized = normalize(path).replaceAll("\\", "/");
|
|
28
|
+
return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
|
|
29
|
+
}
|
|
30
|
+
function validatePathList(input) {
|
|
31
|
+
if (!Array.isArray(input.value)) {
|
|
32
|
+
input.errors.push(`${input.owner} must be an array of relative paths`);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
for (const [index, path] of input.value.entries()) {
|
|
36
|
+
if (typeof path !== "string") {
|
|
37
|
+
input.errors.push(`${input.owner}[${index}] must be a string path`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!isSafeRelativePath(path)) {
|
|
41
|
+
input.errors.push(`${input.owner}[${index}] must stay inside the plugin root`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export function validateFeynmanPluginManifest(value) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
const warnings = [];
|
|
49
|
+
if (!isRecord(value)) {
|
|
50
|
+
return { valid: false, errors: ["manifest must be an object"], warnings };
|
|
51
|
+
}
|
|
52
|
+
for (const key of Object.keys(value)) {
|
|
53
|
+
if (!knownTopLevelKeys.has(key))
|
|
54
|
+
warnings.push(`unknown top-level key ignored by v1: ${key}`);
|
|
55
|
+
}
|
|
56
|
+
if (value.manifest_version !== 1)
|
|
57
|
+
errors.push("manifest_version must be 1");
|
|
58
|
+
if (typeof value.name !== "string" || !isSafePackageName(value.name))
|
|
59
|
+
errors.push("name must be a package-safe identifier without path separators");
|
|
60
|
+
const researchJobs = value.research_jobs;
|
|
61
|
+
if (!Array.isArray(researchJobs) || researchJobs.length === 0) {
|
|
62
|
+
errors.push("research_jobs must be a non-empty array");
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
for (const [index, job] of researchJobs.entries()) {
|
|
66
|
+
if (typeof job !== "string" || !researchJobValues.includes(job)) {
|
|
67
|
+
errors.push(`research_jobs[${index}] is not a supported Feynman research job`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let declaredResource = false;
|
|
72
|
+
if (value.slots !== undefined) {
|
|
73
|
+
if (!isRecord(value.slots)) {
|
|
74
|
+
errors.push("slots must be an object");
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
for (const [slot, paths] of Object.entries(value.slots)) {
|
|
78
|
+
if (!feynmanPluginSlotValues.includes(slot)) {
|
|
79
|
+
errors.push(`unknown plugin slot: ${slot}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (validatePathList({ owner: `slots.${slot}`, value: paths, errors }) && Array.isArray(paths) && paths.length > 0) {
|
|
83
|
+
declaredResource = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (value.pi !== undefined) {
|
|
89
|
+
if (!isRecord(value.pi)) {
|
|
90
|
+
errors.push("pi must be an object");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
for (const [key, paths] of Object.entries(value.pi)) {
|
|
94
|
+
if (!piResourceKeys.has(key)) {
|
|
95
|
+
errors.push(`unknown pi resource: ${key}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (validatePathList({ owner: `pi.${key}`, value: paths, errors }) && Array.isArray(paths) && paths.length > 0) {
|
|
99
|
+
declaredResource = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!declaredResource)
|
|
105
|
+
errors.push("at least one slots or pi resource must be declared");
|
|
106
|
+
if (value.requires_env !== undefined) {
|
|
107
|
+
if (!Array.isArray(value.requires_env)) {
|
|
108
|
+
errors.push("requires_env must be an array");
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
for (const [index, entry] of value.requires_env.entries()) {
|
|
112
|
+
if (typeof entry === "string") {
|
|
113
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(entry))
|
|
114
|
+
errors.push(`requires_env[${index}] must be an environment variable name`);
|
|
115
|
+
}
|
|
116
|
+
else if (isRecord(entry)) {
|
|
117
|
+
if (typeof entry.name !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(entry.name))
|
|
118
|
+
errors.push(`requires_env[${index}].name must be an environment variable name`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
errors.push(`requires_env[${index}] must be a string or object`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
valid: errors.length === 0,
|
|
128
|
+
errors,
|
|
129
|
+
warnings,
|
|
130
|
+
};
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@companion-ai/feynman",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Research-first CLI agent built on Pi and alphaXiv",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@clack/prompts": "^1.5.1",
|
|
75
75
|
"@companion-ai/alpha-hub": "^0.1.3",
|
|
76
|
-
"@earendil-works/pi-agent-core": "0.
|
|
77
|
-
"@earendil-works/pi-ai": "0.
|
|
78
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
79
|
-
"@earendil-works/pi-tui": "0.
|
|
76
|
+
"@earendil-works/pi-agent-core": "0.80.2",
|
|
77
|
+
"@earendil-works/pi-ai": "0.80.2",
|
|
78
|
+
"@earendil-works/pi-coding-agent": "0.80.2",
|
|
79
|
+
"@earendil-works/pi-tui": "0.80.2",
|
|
80
80
|
"@opentelemetry/api": "^1.9.1",
|
|
81
81
|
"@opentelemetry/api-logs": "^0.219.0",
|
|
82
82
|
"@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
|
|
@@ -35,6 +35,81 @@ const OVERFLOW_TRUNCATE_BLOCK = ` let line = newLines[i];
|
|
|
35
35
|
}
|
|
36
36
|
buffer += line;`;
|
|
37
37
|
|
|
38
|
+
const OVERFLOW_THROW_BLOCK_AFTER_CLEAR = ` const line = newLines[i];
|
|
39
|
+
const isImage = isImageLine(line);
|
|
40
|
+
const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i, renderEnd) : 1;
|
|
41
|
+
if (imageReservedRows > 1) {
|
|
42
|
+
const imageStartScreenRow = i - viewportTop;
|
|
43
|
+
if (imageStartScreenRow < 0 || imageStartScreenRow + imageReservedRows > height) {
|
|
44
|
+
logRedraw(\`kitty image pre-clear would scroll (\${imageStartScreenRow} + \${imageReservedRows} > \${height})\`);
|
|
45
|
+
fullRender(true);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
buffer += "\\x1b[2K";
|
|
49
|
+
for (let row = 1; row < imageReservedRows; row++) {
|
|
50
|
+
buffer += "\\r\\n\\x1b[2K";
|
|
51
|
+
}
|
|
52
|
+
buffer += \`\\x1b[\${imageReservedRows - 1}A\`;
|
|
53
|
+
buffer += line;
|
|
54
|
+
buffer += \`\\x1b[\${imageReservedRows - 1}B\`;
|
|
55
|
+
i += imageReservedRows - 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
buffer += "\\x1b[2K"; // Clear current line
|
|
59
|
+
if (!isImage && visibleWidth(line) > width) {
|
|
60
|
+
// Log all lines to crash file for debugging
|
|
61
|
+
const crashLogPath = path.join(os.homedir(), ".pi", "agent", "pi-crash.log");
|
|
62
|
+
const crashData = [
|
|
63
|
+
\`Crash at \${new Date().toISOString()}\`,
|
|
64
|
+
\`Terminal width: \${width}\`,
|
|
65
|
+
\`Line \${i} visible width: \${visibleWidth(line)}\`,
|
|
66
|
+
"",
|
|
67
|
+
"=== All rendered lines ===",
|
|
68
|
+
...newLines.map((l, idx) => \`[\${idx}] (w=\${visibleWidth(l)}) \${l}\`),
|
|
69
|
+
"",
|
|
70
|
+
].join("\\n");
|
|
71
|
+
fs.mkdirSync(path.dirname(crashLogPath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(crashLogPath, crashData);
|
|
73
|
+
// Clean up terminal state before throwing
|
|
74
|
+
this.stop();
|
|
75
|
+
const errorMsg = [
|
|
76
|
+
\`Rendered line \${i} exceeds terminal width (\${visibleWidth(line)} > \${width}).\`,
|
|
77
|
+
"",
|
|
78
|
+
"This is likely caused by a custom TUI component not truncating its output.",
|
|
79
|
+
"Use visibleWidth() to measure and truncateToWidth() to truncate lines.",
|
|
80
|
+
"",
|
|
81
|
+
\`Debug log written to: \${crashLogPath}\`,
|
|
82
|
+
].join("\\n");
|
|
83
|
+
throw new Error(errorMsg);
|
|
84
|
+
}
|
|
85
|
+
buffer += line;`;
|
|
86
|
+
|
|
87
|
+
const OVERFLOW_TRUNCATE_BLOCK_AFTER_CLEAR = ` let line = newLines[i];
|
|
88
|
+
const isImage = isImageLine(line);
|
|
89
|
+
const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i, renderEnd) : 1;
|
|
90
|
+
if (imageReservedRows > 1) {
|
|
91
|
+
const imageStartScreenRow = i - viewportTop;
|
|
92
|
+
if (imageStartScreenRow < 0 || imageStartScreenRow + imageReservedRows > height) {
|
|
93
|
+
logRedraw(\`kitty image pre-clear would scroll (\${imageStartScreenRow} + \${imageReservedRows} > \${height})\`);
|
|
94
|
+
fullRender(true);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
buffer += "\\x1b[2K";
|
|
98
|
+
for (let row = 1; row < imageReservedRows; row++) {
|
|
99
|
+
buffer += "\\r\\n\\x1b[2K";
|
|
100
|
+
}
|
|
101
|
+
buffer += \`\\x1b[\${imageReservedRows - 1}A\`;
|
|
102
|
+
buffer += line;
|
|
103
|
+
buffer += \`\\x1b[\${imageReservedRows - 1}B\`;
|
|
104
|
+
i += imageReservedRows - 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
buffer += "\\x1b[2K"; // Clear current line
|
|
108
|
+
if (!isImage && visibleWidth(line) > width) {
|
|
109
|
+
line = sliceByColumn(line, 0, width, true);
|
|
110
|
+
}
|
|
111
|
+
buffer += line;`;
|
|
112
|
+
|
|
38
113
|
// Two known upstream layouts: pi-tui <=0.75 and the 0.76+ Unicode
|
|
39
114
|
// word-navigation rework. Both need applyBackgroundToLine added for the
|
|
40
115
|
// background-fill render below.
|
|
@@ -200,10 +275,13 @@ export function patchPiTuiSource(source) {
|
|
|
200
275
|
if (source.includes("line = sliceByColumn(line, 0, width, true);")) {
|
|
201
276
|
return source;
|
|
202
277
|
}
|
|
203
|
-
if (
|
|
204
|
-
return source;
|
|
278
|
+
if (source.includes(OVERFLOW_THROW_BLOCK)) {
|
|
279
|
+
return source.replace(OVERFLOW_THROW_BLOCK, OVERFLOW_TRUNCATE_BLOCK);
|
|
280
|
+
}
|
|
281
|
+
if (source.includes(OVERFLOW_THROW_BLOCK_AFTER_CLEAR)) {
|
|
282
|
+
return source.replace(OVERFLOW_THROW_BLOCK_AFTER_CLEAR, OVERFLOW_TRUNCATE_BLOCK_AFTER_CLEAR);
|
|
205
283
|
}
|
|
206
|
-
return source
|
|
284
|
+
return source;
|
|
207
285
|
}
|
|
208
286
|
|
|
209
287
|
export function patchPiEditorSource(source) {
|
|
@@ -24,7 +24,7 @@ const workspacePackageJsonPath = resolve(workspaceDir, "package.json");
|
|
|
24
24
|
const workspaceNpmConfigPath = resolve(workspaceDir, ".npmrc");
|
|
25
25
|
const workspaceArchivePath = resolve(feynmanDir, "runtime-workspace.tgz");
|
|
26
26
|
const PRUNE_VERSION = 8;
|
|
27
|
-
const PI_RUNTIME_FALLBACK_VERSION = "0.
|
|
27
|
+
const PI_RUNTIME_FALLBACK_VERSION = "0.80.2";
|
|
28
28
|
const RUNTIME_PACKAGE_OVERRIDES = {
|
|
29
29
|
"@mozilla/readability": "0.6.0",
|
|
30
30
|
"@opentelemetry/core": "2.8.0",
|