create-academic-research 0.1.16 → 0.1.18

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 (35) hide show
  1. package/README.md +42 -5
  2. package/dist/src/capabilities.d.ts +3 -0
  3. package/dist/src/capabilities.js +106 -21
  4. package/dist/src/cli.js +117 -6
  5. package/dist/src/project.js +161 -4
  6. package/package.json +1 -1
  7. package/template/AGENTS.md +23 -2
  8. package/template/README.md +37 -1
  9. package/template/_gitignore +6 -0
  10. package/template/analysis_outputs/claim-audit.md +7 -0
  11. package/template/artifacts/artifact-checklist.md +54 -2
  12. package/template/artifacts/badge-evidence-ledger.csv +1 -0
  13. package/template/docs/agent/mcp-client-setup.md +12 -8
  14. package/template/docs/agent/mcp-setup.md +23 -0
  15. package/template/docs/agent/output-contracts.md +49 -5
  16. package/template/docs/agent/project-quality.md +80 -0
  17. package/template/docs/agent/repo-migration-playbook.md +20 -0
  18. package/template/docs/getting-started.md +37 -8
  19. package/template/docs/reproducibility/commands.md +12 -0
  20. package/template/experiments/campaigns/autonomous-campaign-template.md +68 -0
  21. package/template/experiments/campaigns/frontier-results.tsv +1 -0
  22. package/template/package.json +2 -1
  23. package/template/reports/paper/sota-survey.tex +29 -0
  24. package/template/repro_outputs/COMMANDS.md +4 -0
  25. package/template/repro_outputs/LOG.md +6 -0
  26. package/template/repro_outputs/PATCHES.md +7 -0
  27. package/template/repro_outputs/SUMMARY.md +21 -0
  28. package/template/repro_outputs/status.json +9 -0
  29. package/template/sota/citation-chasing-log.csv +1 -0
  30. package/template/sota/literature-matrix.csv +1 -1
  31. package/template/sota/paper-syntheses/.gitkeep +1 -0
  32. package/template/sota/reading-log.csv +1 -0
  33. package/template/sota/search-strategy.md +29 -0
  34. package/template/sources/markdown-linear/.gitkeep +1 -0
  35. package/template/tests/test_project_structure.py +18 -0
package/README.md CHANGED
@@ -41,7 +41,7 @@ npx --yes github:VincenzoImp/create-academic-research my-project
41
41
  | Sources | PDFs, derived Markdown, metadata, BibTeX, conversion ledger, source ledger. |
42
42
  | Literature Review | Search strategy, screening decisions, literature matrix, SOTA synthesis, gaps, PRISMA flow. |
43
43
  | Agent Memory | `AGENTS.md`, capability profile, MCP setup docs, generated MCP snippets, wiki index/log/templates. |
44
- | Reproducibility | Python package scaffold, tests, experiment registry, output folders, artifact checklist. |
44
+ | Reproducibility | Python package scaffold, tests, experiment registry, autonomous campaign ledgers, output folders, artifact checklist. |
45
45
  | Skills | Project-local installation flow for `VincenzoImp/academic-research-skills`. |
46
46
  | MCP | Conservative default records for scholarly discovery plus documented optional integrations. |
47
47
 
@@ -115,7 +115,9 @@ Inside a generated project:
115
115
  ```bash
116
116
  npm run doctor
117
117
  npm run update
118
- npm run setup
118
+ npm run update -- --apply
119
+ npm run setup -- --env-file .env.local
120
+ npm run workflow:literature
119
121
  npm run rename -- --title "New Title" --slug new-title --package new_title
120
122
  npm run agents:list
121
123
  npm run skills:presets
@@ -173,13 +175,32 @@ Safe migrations are tracked in `.academic-research/managed-files.json`. The
173
175
  manifest stores non-secret checksums for generator-owned files. If a file was
174
176
  edited locally, update reports `skip` instead of overwriting it.
175
177
 
178
+ ### Migration 0.1.17 -> 0.1.18
179
+
180
+ Projects created with `0.1.17` can migrate in place:
181
+
182
+ ```bash
183
+ npm run update
184
+ npm run update -- --apply
185
+ npm run doctor
186
+ ```
187
+
188
+ The migration adds the project-quality contract, badge evidence ledger, SOTA
189
+ reading and citation-chasing ledgers, paper synthesis folders, linear reading
190
+ copies, autonomous experiment campaign files, claim-audit and reproduction
191
+ templates, and `workflow:literature`. Locally edited managed files are skipped;
192
+ new research record templates are created as user-owned where the project is
193
+ expected to fill them over time.
194
+
176
195
  `academic-research init` initializes an existing repository without overwriting
177
196
  existing files. It adds the research contract, merges lifecycle package scripts,
178
197
  and preserves existing README, `.gitignore`, and custom package scripts.
179
198
 
180
- `academic-research setup` is a non-destructive onboarding status command. It
199
+ `academic-research setup` is the friendly post-update recovery command. It
181
200
  prints the active preset, agent, skill counts, selected MCP records, and next
182
- commands without changing files.
201
+ commands. When you pass `--env-file .env.local`, it may complete safe
202
+ project-local setup such as the Overleaf wrapper and generated MCP snippet. It
203
+ does not register global MCP clients.
183
204
 
184
205
  Skills are project-local by default.
185
206
 
@@ -205,7 +226,7 @@ MCP commands are split by side-effect:
205
226
  | `mcp env` | Print required/recommended env vars, hosted endpoints, local prerequisites, and setup commands for selected servers. Use `--dotenv --all` to print dotenv content or `--write .env.example --all` to regenerate `.env.example`. |
206
227
  | `mcp enable` | Select an MCP server in project records and generated snippets. Use `--mode local`, `--mode remote`, or `--mode remote-custom --url <url>` where supported. |
207
228
  | `mcp disable` | Remove an MCP server from project records and generated snippets. |
208
- | `mcp setup` | Run or dry-run finite setup for manual-local integrations such as Overleaf. |
229
+ | `mcp setup` | Run or dry-run finite project-local setup for manual-local integrations such as Overleaf, including wrapper and generated snippet refresh. |
209
230
  | `mcp client add` / `mcp client remove` | Register or remove supported MCP client entries, currently Codex, without writing secrets into client config. |
210
231
  | `mcp install` | Run finite external tool install commands for selected MCP servers. It must not launch stdio MCP servers. |
211
232
  | `mcp uninstall` | Run the external uninstall command when one exists. |
@@ -213,6 +234,11 @@ MCP commands are split by side-effect:
213
234
  | `mcp doctor` | Validate enabled MCP records, generated snippets, required env vars, and documented manual prerequisites. Pass `--env-file .env.local` to read explicit local secrets. |
214
235
  | `mcp probe` | Opt-in runtime check. Local stdio servers get a JSON-RPC handshake; remote endpoints are reported as configured without a network probe. |
215
236
 
237
+ Workflow commands are scenario-level shortcuts over skills and MCP records.
238
+ Use `npm run workflow:literature` when starting a serious SOTA, survey, or
239
+ related-work pass: it selects a practical literature stack with arXiv, DBLP,
240
+ Semantic Scholar, and OpenAlex remote graph search, then prints the next checks.
241
+
216
242
  ## Companion Skills
217
243
 
218
244
  The generated project works best with:
@@ -266,6 +292,17 @@ a generated safe dotenv-loading wrapper after `mcp setup`.
266
292
  Crossref and broad paper-search aggregators are kept as fallback/manual entries
267
293
  until a project explicitly needs them.
268
294
 
295
+ For SOTA work, the recommended low-friction path is:
296
+
297
+ ```bash
298
+ npm run workflow:literature
299
+ npm run skills:install -- --preset literature
300
+ npm run mcp:status
301
+ npm run mcp:smoke -- --env-file .env.local
302
+ ```
303
+
304
+ Then use `$sota-literature-review` with a declared review scale and seed set.
305
+
269
306
  Codex automatic registration supports custom remote endpoints when the URL is
270
307
  stored in project config with `--url`. If the endpoint URL is kept private via
271
308
  `--url-env`, Codex automatic registration is not available because the Codex CLI
@@ -166,6 +166,7 @@ interface SkillInstallOptions {
166
166
  }
167
167
  export declare function readCapabilities(root: string): Promise<CapabilityState>;
168
168
  export declare function writeCapabilities(root: string, state: Partial<CapabilityState>): Promise<void>;
169
+ export declare function writeCapabilityGeneratedFiles(root: string, state: CapabilityState): Promise<void>;
169
170
  export declare function writeMcpEnvironmentExample(root: string): Promise<void>;
170
171
  export declare function initializeCapabilities(root: string, options?: InitializeCapabilitiesOptions): Promise<void>;
171
172
  export declare function buildSkillInstallCommands(root: string, preset?: string, options?: SkillInstallOptions): Promise<string[][]>;
@@ -198,4 +199,6 @@ export declare function setupMcpServer(root: string, serverName: string, options
198
199
  export declare function clientAddMcpServer(root: string, serverName: string, options?: McpClientOptions, runner?: Runner): Promise<McpClientResult>;
199
200
  export declare function clientRemoveMcpServer(root: string, serverName: string, options?: McpClientOptions, runner?: Runner): Promise<McpClientResult>;
200
201
  export declare function resolveMcpServerForState(state: CapabilityState, serverName: string, mode?: string): ResolvedMcpServer;
202
+ export declare function mcpMissingGeneratedSnippetMessage(serverName: string, server: ResolvedMcpServer, env?: NodeJS.ProcessEnv): string;
203
+ export declare function mcpLocalSetupGitignoreWarning(root: string): Promise<string | undefined>;
201
204
  export declare function assertKnownMcpServers(servers: string[]): void;
@@ -24,21 +24,16 @@ export async function readCapabilities(root) {
24
24
  }
25
25
  }
26
26
  export async function writeCapabilities(root, state) {
27
- const mcpServers = [...(state.mcp_servers ?? [])];
28
- const next = {
29
- agent: assertKnownAgentTarget(state.agent),
30
- preset: state.preset ?? "default",
31
- scope: "project-local",
32
- mcp_servers: mcpServers,
33
- mcp_server_modes: normalizeMcpServerModeMap(state.mcp_server_modes ?? {}, mcpServers),
34
- mcp_server_remote: normalizeMcpServerRemoteMap(state.mcp_server_remote ?? {}, mcpServers, state.mcp_server_modes ?? {})
35
- };
36
- await writeFile(join(root, "configs/capabilities.yaml"), YAML.stringify(serializeCapabilityState(next)), "utf8");
37
- await writeCapabilityProfile(root, next);
38
- await writeMcpSetup(root, next);
39
- await writeMcpSnippet(root, next);
27
+ const next = normalizeCapabilityWriteState(state);
28
+ await writeCapabilityConfig(root, next);
29
+ await writeCapabilityGeneratedFiles(root, next);
40
30
  await appendCapabilityLog(root, next);
41
31
  }
32
+ export async function writeCapabilityGeneratedFiles(root, state) {
33
+ await writeCapabilityProfile(root, state);
34
+ await writeMcpSetup(root, state);
35
+ await writeMcpSnippet(root, state);
36
+ }
42
37
  export async function writeMcpEnvironmentExample(root) {
43
38
  await writeFile(join(root, ".env.example"), formatMcpDotenv(Object.keys(AGENT_STACK.mcp_servers)), "utf8");
44
39
  }
@@ -321,6 +316,18 @@ export async function doctorMcpServers(root, options = {}) {
321
316
  errors.push(`invalid generated MCP snippet: ${snippetPath}: ${message}`);
322
317
  }
323
318
  }
319
+ const lock = await readCapabilityLock(root);
320
+ const hasManualLocal = enabled.some((name) => {
321
+ if (!AGENT_STACK.mcp_servers[name])
322
+ return false;
323
+ const server = resolveMcpServerForState(state, name, modes[name]);
324
+ return server.connection_mode === "manual-local";
325
+ });
326
+ if (hasManualLocal) {
327
+ const gitignoreWarning = await mcpLocalSetupGitignoreWarning(root);
328
+ if (gitignoreWarning)
329
+ warnings.push(gitignoreWarning);
330
+ }
324
331
  for (const name of enabled) {
325
332
  const server = resolveMcpServerForState(state, name, modes[name]);
326
333
  if (!server)
@@ -339,7 +346,6 @@ export async function doctorMcpServers(root, options = {}) {
339
346
  warnings.push(`${name}: requires local service: ${server.local_service}`);
340
347
  }
341
348
  if (server.connection_mode === "manual-local") {
342
- const lock = await readCapabilityLock(root);
343
349
  if (lock.mcp[name]?.setup?.status !== "ready") {
344
350
  warnings.push(`${name}: local setup not complete; run npm run mcp:setup -- ${name} --mode local --env-file .env.local`);
345
351
  }
@@ -349,7 +355,14 @@ export async function doctorMcpServers(root, options = {}) {
349
355
  continue;
350
356
  }
351
357
  if (!generatedServers.has(name)) {
352
- errors.push(`${name}: enabled but missing from generated MCP snippet`);
358
+ errors.push(mcpMissingGeneratedSnippetMessage(name, server, env));
359
+ continue;
360
+ }
361
+ if (state.agent === "codex" &&
362
+ server.connection_mode === "manual-local" &&
363
+ lock.mcp[name]?.setup?.status === "ready" &&
364
+ lock.mcp[name]?.clients?.codex?.status !== "registered") {
365
+ warnings.push(`${name} is setup locally but not registered in Codex\nNEXT: npm run mcp:client:add -- ${name} --agent codex`);
353
366
  }
354
367
  }
355
368
  return { ok: errors.length === 0, errors, warnings, enabled };
@@ -662,8 +675,15 @@ export async function readCapabilityLock(root) {
662
675
  }
663
676
  export async function setupMcpServer(root, serverName, options = {}, runner = defaultRunner) {
664
677
  assertKnownMcpServers([serverName]);
665
- const mode = normalizeMcpMode(serverName, options.mode);
666
- const server = resolveMcpServer(serverName, mode);
678
+ const state = await readCapabilities(root);
679
+ const mode = normalizeMcpMode(serverName, options.mode ?? state.mcp_server_modes?.[serverName]);
680
+ const nextState = normalizeCapabilityWriteState({
681
+ ...state,
682
+ mcp_servers: dedupe([...(state.mcp_servers ?? []), serverName]),
683
+ mcp_server_modes: { ...(state.mcp_server_modes ?? {}), [serverName]: mode },
684
+ mcp_server_remote: state.mcp_server_remote ?? {}
685
+ });
686
+ const server = resolveMcpServerForState(nextState, serverName, mode);
667
687
  if (serverName !== "overleaf") {
668
688
  const commands = server.setup_commands.length > 0 ? server.setup_commands : [];
669
689
  return {
@@ -686,6 +706,7 @@ export async function setupMcpServer(root, serverName, options = {}, runner = de
686
706
  `cd ${paths.relativeServer} && uv sync`,
687
707
  `write wrapper ${paths.relativeWrapper}`
688
708
  ];
709
+ const gitignoreWarning = await mcpLocalSetupGitignoreWarning(root);
689
710
  if (missing.length > 0) {
690
711
  return {
691
712
  ok: false,
@@ -693,7 +714,7 @@ export async function setupMcpServer(root, serverName, options = {}, runner = de
693
714
  mode,
694
715
  commands,
695
716
  created: [],
696
- warnings: [],
717
+ warnings: gitignoreWarning ? [gitignoreWarning] : [],
697
718
  errors: [`overleaf: missing required environment variable(s): ${missing.join(", ")}`],
698
719
  next: [`fill ${missing.join(", ")} in ${options.envFile ?? ".env.local"}`]
699
720
  };
@@ -705,7 +726,7 @@ export async function setupMcpServer(root, serverName, options = {}, runner = de
705
726
  mode,
706
727
  commands,
707
728
  created: [],
708
- warnings: [],
729
+ warnings: gitignoreWarning ? [gitignoreWarning] : [],
709
730
  errors: [],
710
731
  next: [`run npm run mcp:setup -- overleaf --mode local --env-file ${options.envFile ?? ".env.local"}`]
711
732
  };
@@ -727,17 +748,19 @@ export async function setupMcpServer(root, serverName, options = {}, runner = de
727
748
  status: "ready",
728
749
  server_path: paths.relativeServer,
729
750
  wrapper_path: paths.relativeWrapper,
730
- env_file: options.envFile ? toPosix(relative(root, resolve(root, options.envFile))) : ".env.local",
751
+ env_file: mcpEnvFileRecord(root, options.envFile),
731
752
  updated_at: nowIso()
732
753
  };
733
754
  });
755
+ await writeCapabilityConfig(root, nextState);
756
+ await writeCapabilityGeneratedFiles(root, nextState);
734
757
  return {
735
758
  ok: true,
736
759
  server: serverName,
737
760
  mode,
738
761
  commands,
739
762
  created: [paths.relativeWrapper, paths.relativeLauncher],
740
- warnings: [],
763
+ warnings: gitignoreWarning ? [gitignoreWarning] : [],
741
764
  errors: [],
742
765
  next: [
743
766
  "npm run mcp:client:add -- overleaf --agent codex",
@@ -824,6 +847,20 @@ function serializeCapabilityState(state) {
824
847
  serialized.mcp_server_remote = state.mcp_server_remote;
825
848
  return serialized;
826
849
  }
850
+ function normalizeCapabilityWriteState(state) {
851
+ const mcpServers = [...(state.mcp_servers ?? [])];
852
+ return {
853
+ agent: assertKnownAgentTarget(state.agent),
854
+ preset: state.preset ?? "default",
855
+ scope: "project-local",
856
+ mcp_servers: mcpServers,
857
+ mcp_server_modes: normalizeMcpServerModeMap(state.mcp_server_modes ?? {}, mcpServers),
858
+ mcp_server_remote: normalizeMcpServerRemoteMap(state.mcp_server_remote ?? {}, mcpServers, state.mcp_server_modes ?? {})
859
+ };
860
+ }
861
+ async function writeCapabilityConfig(root, state) {
862
+ await writeFile(join(root, "configs/capabilities.yaml"), YAML.stringify(serializeCapabilityState(state)), "utf8");
863
+ }
827
864
  function normalizeMcpServerModeMap(modes, servers) {
828
865
  const selected = new Set(servers);
829
866
  const result = {};
@@ -986,6 +1023,19 @@ function mcpModeKeyLabelForAction(mode) {
986
1023
  return "manual setup";
987
1024
  return "local";
988
1025
  }
1026
+ export function mcpMissingGeneratedSnippetMessage(serverName, server, env = process.env) {
1027
+ const lines = [`${serverName}: enabled but missing from generated MCP snippet`];
1028
+ if (server.connection_mode === "manual-local") {
1029
+ lines.push(`NEXT: npm run mcp:setup -- ${serverName} --mode local --env-file .env.local`);
1030
+ const missing = server.required_env.filter((name) => !envHasValue(env, name));
1031
+ if (missing.length > 0)
1032
+ lines.push(`Missing env vars: ${missing.join(", ")}`);
1033
+ }
1034
+ else {
1035
+ lines.push("NEXT: npm run update -- --apply");
1036
+ }
1037
+ return lines.join("\n");
1038
+ }
989
1039
  function mcpInstallSkip(serverName, server) {
990
1040
  if (server.connection_mode === "manual-local") {
991
1041
  return {
@@ -1098,6 +1148,41 @@ function ensureLockMcpEntry(lock, serverName, server) {
1098
1148
  lock.mcp[serverName] = entry;
1099
1149
  return entry;
1100
1150
  }
1151
+ export async function mcpLocalSetupGitignoreWarning(root) {
1152
+ if (await mcpLocalSetupPathIsIgnored(root))
1153
+ return undefined;
1154
+ return [
1155
+ ".academic-research/mcp/ is not ignored",
1156
+ "NEXT: add .academic-research/mcp/ to .gitignore before committing local MCP server files"
1157
+ ].join("\n");
1158
+ }
1159
+ async function mcpLocalSetupPathIsIgnored(root) {
1160
+ try {
1161
+ const gitignore = await readFile(join(root, ".gitignore"), "utf8");
1162
+ return gitignore
1163
+ .split(/\r?\n/)
1164
+ .map((line) => line.trim())
1165
+ .filter((line) => line && !line.startsWith("#"))
1166
+ .some((line) => {
1167
+ const normalized = line.replaceAll("\\", "/").replace(/^\/+/, "");
1168
+ return normalized === ".academic-research/mcp/" || normalized === ".academic-research/mcp";
1169
+ });
1170
+ }
1171
+ catch (error) {
1172
+ if (isMissingFileError(error))
1173
+ return false;
1174
+ throw error;
1175
+ }
1176
+ }
1177
+ function mcpEnvFileRecord(root, envFile) {
1178
+ if (!envFile)
1179
+ return ".env.local";
1180
+ const resolved = resolve(root, envFile);
1181
+ const relativePath = relative(root, resolved);
1182
+ if (!relativePath.startsWith("..") && !isAbsolute(relativePath))
1183
+ return toPosix(relativePath);
1184
+ return ".env.local";
1185
+ }
1101
1186
  function overleafPaths(root) {
1102
1187
  const relativeBase = ".academic-research/mcp/overleaf";
1103
1188
  const wrapperDir = join(root, relativeBase);
package/dist/src/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, delimiter, dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { assertKnownMcpServers, clientAddMcpServer, clientRemoveMcpServer, disableMcpServers, doctorMcpServers, enableMcpServers, DEFAULT_AGENT, getMcpLifecycleStatus, installMcpTools, installSkillIds, installSkills, formatMcpDotenv, listInstalledSkills, listMcpEnvironmentEntries, mergeMcpEnvironment, mcpToolCommandTexts, probeMcpServers, readCapabilities, readMcpEnvironmentFile, removeSkills, resolveMcpServerForState, setupMcpServer, uninstallMcpTools, updateSkills } from "./capabilities.js";
4
+ import { assertKnownMcpServers, clientAddMcpServer, clientRemoveMcpServer, disableMcpServers, doctorMcpServers, enableMcpServers, DEFAULT_AGENT, getMcpLifecycleStatus, installMcpTools, installSkillIds, installSkills, formatMcpDotenv, listInstalledSkills, listMcpEnvironmentEntries, mergeMcpEnvironment, mcpToolCommandTexts, probeMcpServers, readCapabilities, readMcpEnvironmentFile, removeSkills, resolveMcpServerForState, setupMcpServer, uninstallMcpTools, updateSkills, writeCapabilities } from "./capabilities.js";
5
5
  import { createProject, doctorProject, initProject, renameProject, updateProject } from "./project.js";
6
6
  import { askCreateOptions } from "./prompts.js";
7
7
  import { AGENT_STACK, mcpModeLabel, mcpRecommendedMode, mcpServerModeKeys, mcpSupportedModeLabels, presetMcpServers, resolveMcpServer } from "./stack.js";
@@ -19,6 +19,8 @@ const CREATE_FLAGS = flagSchema([
19
19
  "no-install-mcp-tools"
20
20
  ], ["title", "slug", "package", "preset", "profile", "agent"]);
21
21
  const ROOT_FLAGS = flagSchema(["help"], ["root"]);
22
+ const SETUP_FLAGS = flagSchema(["help"], ["root", "env-file"]);
23
+ const WORKFLOW_FLAGS = flagSchema(["help"], ["root", "agent", "env-file"]);
22
24
  const UPDATE_FLAGS = flagSchema(["help", "dry-run", "apply"], ["root"]);
23
25
  const INIT_FLAGS = flagSchema(["help", "install-skills"], ["root", "title", "slug", "package", "preset", "profile", "agent"]);
24
26
  const RENAME_FLAGS = flagSchema(["help"], ["root", "title", "slug", "package"]);
@@ -129,6 +131,8 @@ async function lifecycleMain(argv) {
129
131
  return skillsCommand(argv.slice(1));
130
132
  if (command === "mcp")
131
133
  return mcpCommand(argv.slice(1));
134
+ if (command === "workflow")
135
+ return workflowCommand(argv.slice(1));
132
136
  printLifecycleHelp();
133
137
  return command === "help" ? 0 : 1;
134
138
  }
@@ -173,6 +177,12 @@ async function updateCommand(argv) {
173
177
  if (!apply && result.changes.length > 0) {
174
178
  console.log("Run `npm run update -- --apply` from a generated project to write these managed changes.");
175
179
  }
180
+ if (apply && await projectLocalMcpSetupNeeded(root)) {
181
+ console.log("");
182
+ console.log("Next:");
183
+ console.log("1. Run npm run setup -- --env-file .env.local to complete project-local tool setup.");
184
+ console.log("2. Run npm run doctor to verify the project.");
185
+ }
176
186
  return 0;
177
187
  }
178
188
  async function initCommand(argv) {
@@ -198,13 +208,15 @@ async function initCommand(argv) {
198
208
  return 0;
199
209
  }
200
210
  async function setupCommand(argv) {
201
- const parsed = parseFlags(argv, ROOT_FLAGS);
211
+ const parsed = parseFlags(argv, SETUP_FLAGS);
202
212
  if (flagBool(parsed.flags, "help")) {
203
213
  printSetupHelp();
204
214
  return 0;
205
215
  }
206
216
  assertNoArguments(parsed.positionals, "setup");
207
217
  const root = resolve(flagString(parsed.flags, "root") ?? ".");
218
+ const env = await mcpCommandEnvironment(root, parsed.flags);
219
+ const setupResults = await runProjectLocalMcpSetup(root, env, flagString(parsed.flags, "env-file"));
208
220
  const project = await doctorProject(root);
209
221
  const state = await readCapabilities(root);
210
222
  const skills = await listInstalledSkills(root);
@@ -219,13 +231,30 @@ async function setupCommand(argv) {
219
231
  console.log(`installed_skill_copies\t${skills.length}`);
220
232
  console.log(`mcp_enabled\t${state.mcp_servers.length > 0 ? state.mcp_servers.join(",") : "none"}`);
221
233
  console.log(`mcp_selected\t${state.mcp_servers.length > 0 ? state.mcp_servers.join(",") : "none"}`);
234
+ for (const result of setupResults) {
235
+ if (result.ok) {
236
+ console.log(`Completed project-local MCP setup: ${result.server}`);
237
+ }
238
+ }
222
239
  if (!project.ok) {
223
240
  for (const error of project.errors)
224
241
  console.error(`ERROR: ${error}`);
225
242
  }
226
- for (const warning of project.warnings)
243
+ const setupWarnings = [];
244
+ for (const result of setupResults) {
245
+ for (const error of result.errors)
246
+ console.error(`ERROR: ${error}`);
247
+ setupWarnings.push(...result.warnings);
248
+ if (!result.ok && result.next.length > 0) {
249
+ console.log("");
250
+ console.log(`Next for ${result.server}`);
251
+ for (const command of result.next)
252
+ console.log(command);
253
+ }
254
+ }
255
+ for (const warning of dedupeStrings([...setupWarnings, ...project.warnings]))
227
256
  console.warn(`WARN: ${warning}`);
228
- const lifecycle = await getMcpLifecycleStatus(root);
257
+ const lifecycle = await getMcpLifecycleStatus(root, { env });
229
258
  console.log("");
230
259
  console.log("Next Commands");
231
260
  console.log(`npm run skills:install -- --preset ${state.preset}`);
@@ -598,6 +627,49 @@ async function mcpCommand(argv) {
598
627
  }
599
628
  throw new Error(`unknown mcp command: ${subcommand}`);
600
629
  }
630
+ async function workflowCommand(argv) {
631
+ const subcommand = argv[0] ?? "help";
632
+ const parsed = parseFlags(argv.slice(1), WORKFLOW_FLAGS);
633
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h" || flagBool(parsed.flags, "help")) {
634
+ printWorkflowHelp();
635
+ return 0;
636
+ }
637
+ if (subcommand !== "literature")
638
+ throw new Error(`unknown workflow command: ${subcommand}`);
639
+ assertNoArguments(parsed.positionals, "workflow literature");
640
+ const root = resolve(flagString(parsed.flags, "root") ?? ".");
641
+ const state = await readCapabilities(root);
642
+ const agent = flagString(parsed.flags, "agent") ?? state.agent;
643
+ const literatureServers = ["arxiv", "dblp", "semantic-scholar", "openalex"];
644
+ await writeCapabilities(root, {
645
+ ...state,
646
+ agent,
647
+ preset: "literature",
648
+ mcp_servers: literatureServers,
649
+ mcp_server_modes: {
650
+ ...state.mcp_server_modes,
651
+ openalex: "remote"
652
+ },
653
+ mcp_server_remote: state.mcp_server_remote
654
+ });
655
+ const env = await mcpCommandEnvironment(root, parsed.flags);
656
+ const lifecycle = await getMcpLifecycleStatus(root, { env });
657
+ const selected = lifecycle.servers.filter((server) => server.selected);
658
+ console.log("Literature Workflow");
659
+ console.log(`root\t${root}`);
660
+ console.log("preset\tliterature");
661
+ console.log(`mcp_selected\t${literatureServers.join(",")}`);
662
+ for (const item of selected) {
663
+ console.log(`mcp\t${item.id}\t${item.mode}\t${item.state}\t${friendlyNext(item.next)}`);
664
+ }
665
+ console.log("");
666
+ console.log("Next Commands");
667
+ console.log("npm run skills:install -- --preset literature");
668
+ console.log("npm run mcp:status");
669
+ console.log("npm run mcp:smoke -- --env-file .env.local");
670
+ console.log("Use $sota-literature-review with a declared scale, seed set, and citation-chasing budget.");
671
+ return 0;
672
+ }
601
673
  async function mcpClientCommand(parsed) {
602
674
  const action = parsed.positionals[0];
603
675
  const server = parsed.positionals[1];
@@ -649,6 +721,28 @@ function setupNextCommands(item) {
649
721
  commands.push(item.next.replace(/^run /, ""));
650
722
  return dedupeStrings(commands);
651
723
  }
724
+ async function runProjectLocalMcpSetup(root, env, envFile) {
725
+ const lifecycle = await getMcpLifecycleStatus(root, { env });
726
+ const results = [];
727
+ for (const item of lifecycle.servers) {
728
+ if (!item.selected || item.connection_mode !== "manual-local")
729
+ continue;
730
+ if (item.install === "ready" && item.snippet !== "missing")
731
+ continue;
732
+ results.push(await setupMcpServer(root, item.id, {
733
+ mode: item.mode_key,
734
+ envFile: envFile ?? ".env.local",
735
+ env
736
+ }));
737
+ }
738
+ return results;
739
+ }
740
+ async function projectLocalMcpSetupNeeded(root) {
741
+ const lifecycle = await getMcpLifecycleStatus(root);
742
+ return lifecycle.servers.some((item) => item.selected &&
743
+ item.connection_mode === "manual-local" &&
744
+ (item.install !== "ready" || item.snippet === "missing"));
745
+ }
652
746
  function dedupeStrings(values) {
653
747
  return [...new Set(values.filter(Boolean))];
654
748
  }
@@ -899,7 +993,7 @@ function printMissingTargetHelp() {
899
993
  }
900
994
  function printLifecycleHelp() {
901
995
  console.log([
902
- "Usage: academic-research <doctor|update|init|setup|rename|agents|skills|mcp>",
996
+ "Usage: academic-research <doctor|update|init|setup|rename|agents|skills|mcp|workflow>",
903
997
  "",
904
998
  "Manage a generated academic research repository after creation.",
905
999
  "",
@@ -908,6 +1002,22 @@ function printLifecycleHelp() {
908
1002
  " -v, --version Show package version."
909
1003
  ].join("\n"));
910
1004
  }
1005
+ function printWorkflowHelp() {
1006
+ console.log([
1007
+ "Usage: academic-research workflow <literature> [options]",
1008
+ "",
1009
+ "Prepare scenario-level research workflows without manually stitching every skill and MCP command.",
1010
+ "",
1011
+ "Workflows:",
1012
+ " literature Configure the practical SOTA stack for arXiv, DBLP, Semantic Scholar citation graph, and OpenAlex graph search.",
1013
+ "",
1014
+ "Options:",
1015
+ " --root <path> Project root. Default: current directory.",
1016
+ " --agent <id> Agent target for generated MCP snippets.",
1017
+ " --env-file <path> Read local env values for readiness reporting.",
1018
+ " -h, --help Show this help."
1019
+ ].join("\n"));
1020
+ }
911
1021
  function printUpdateHelp() {
912
1022
  console.log([
913
1023
  "Usage: academic-research update [options]",
@@ -943,10 +1053,11 @@ function printSetupHelp() {
943
1053
  console.log([
944
1054
  "Usage: academic-research setup [options]",
945
1055
  "",
946
- "Print project onboarding status and next commands without changing files.",
1056
+ "Print project onboarding status and complete safe project-local setup when possible.",
947
1057
  "",
948
1058
  "Options:",
949
1059
  " --root <path> Project root. Default: current directory.",
1060
+ " --env-file <path> Read local env values for guided project-local MCP setup.",
950
1061
  " -h, --help Show this help."
951
1062
  ].join("\n"));
952
1063
  }