erdos-problems 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -18,9 +18,11 @@ Official binary:
18
18
 
19
19
  - atlas layer with canonical local `problems/<id>/problem.yaml` records
20
20
  - bundled upstream snapshot from `teorth/erdosproblems`
21
- - workspace `.erdos/` state for active-problem selection, upstream refreshes, reports, and scaffolds
21
+ - workspace `.erdos/` state for active-problem selection, upstream refreshes, reports, scaffolds, and pull bundles
22
22
  - sunflower cluster as the first deep harness pack
23
+ - packaged compute-lane metadata for deep sunflower problems, surfaced directly in the CLI
23
24
  - seeded atlas now includes open and solved problems beyond sunflower
25
+ - unseeded problems can still be pulled into a workspace from the bundled upstream snapshot
24
26
 
25
27
  Seeded problems:
26
28
  - `18`, `20`, `89`, `536`, `542`, `856`, `857`, `1008`
@@ -30,7 +32,7 @@ Seeded problems:
30
32
  ```bash
31
33
  erdos problem list --cluster sunflower
32
34
  erdos bootstrap problem 857
33
- erdos problem artifacts 857
35
+ erdos problem artifacts 857 --json
34
36
  erdos dossier show 857
35
37
  ```
36
38
 
@@ -40,6 +42,22 @@ What `bootstrap` does:
40
42
  - includes the upstream record when a bundled or workspace snapshot is available
41
43
  - gives an agent a ready-to-read local artifact bundle immediately after install
42
44
 
45
+ ## Pull bundles
46
+
47
+ For any problem number in the upstream snapshot, you can create a workspace bundle even if the problem is not yet seeded locally:
48
+
49
+ ```bash
50
+ erdos pull problem 857
51
+ erdos pull problem 999 --include-site
52
+ erdos pull problem 999 --refresh-upstream
53
+ ```
54
+
55
+ What `pull` does:
56
+ - creates `.erdos/pulls/<id>/`
57
+ - includes the upstream record when available
58
+ - includes the local canonical dossier too when the problem is seeded locally
59
+ - can optionally add a live site snapshot and plain-text extract
60
+
43
61
  ## CLI
44
62
 
45
63
  ```bash
@@ -55,6 +73,8 @@ erdos problem artifacts 857 --json
55
73
  erdos cluster list
56
74
  erdos cluster show sunflower
57
75
  erdos workspace show
76
+ erdos sunflower status 857
77
+ erdos sunflower status --json
58
78
  erdos dossier show
59
79
  erdos upstream show
60
80
  erdos upstream sync
@@ -62,6 +82,8 @@ erdos upstream diff
62
82
  erdos scaffold problem 857
63
83
  erdos bootstrap problem 857
64
84
  erdos bootstrap problem 857 --sync-upstream
85
+ erdos pull problem 857
86
+ erdos pull problem 857 --include-site
65
87
  ```
66
88
 
67
89
  ## Canonical Sources
@@ -84,8 +106,14 @@ For each seeded problem, the canonical local dossier lives in `problems/<id>/`:
84
106
  The CLI can surface these directly:
85
107
  - `erdos problem artifacts <id>` shows the canonical inventory
86
108
  - `erdos problem artifacts <id> --json` emits machine-readable inventory
87
- - `erdos scaffold problem <id>` copies the bundle into the active workspace
109
+ - `erdos scaffold problem <id>` copies the seeded dossier into the active workspace
88
110
  - `erdos bootstrap problem <id>` selects the problem and creates the scaffold in one step
111
+ - `erdos pull problem <id>` creates a workspace bundle for any problem in the upstream snapshot
112
+
113
+ For deep sunflower problems, the CLI also surfaces packaged compute lanes:
114
+ - `erdos sunflower status <id>` shows route + compute posture together
115
+ - `erdos workspace show` includes the active sunflower compute summary when applicable
116
+ - `erdos scaffold problem <id>` copies packaged compute packets into `.erdos/scaffolds/<id>/COMPUTE/`
89
117
 
90
118
  ## Notes
91
119
 
@@ -12,10 +12,11 @@ The goal is:
12
12
  - open and solved problems use the same shape
13
13
  - local dossier truth and upstream public truth stay explicitly separated
14
14
  - packaged CLI installs can scaffold problem workspaces from canonical artifacts immediately
15
+ - unseeded problems can still be pulled into a workspace bundle from upstream truth
15
16
 
16
17
  ## Canonical Files
17
18
 
18
- Each problem should have:
19
+ Each seeded problem should have:
19
20
 
20
21
  - `problems/<id>/problem.yaml`
21
22
  - `problems/<id>/STATEMENT.md`
@@ -29,6 +30,12 @@ Bundled upstream snapshot artifacts live in:
29
30
  - `data/upstream/erdosproblems/PROBLEMS_INDEX.json`
30
31
  - `data/upstream/erdosproblems/SYNC_MANIFEST.json`
31
32
 
33
+ Workspace-generated artifacts may live in:
34
+
35
+ - `.erdos/scaffolds/<id>/`
36
+ - `.erdos/pulls/<id>/`
37
+ - `.erdos/upstream/erdosproblems/`
38
+
32
39
  ## Canonical Truth Split
33
40
 
34
41
  ### External public truth
@@ -178,4 +185,15 @@ The sync commands should produce:
178
185
  - upstream record snapshot for that problem when available
179
186
  - generated artifact index for agent consumption
180
187
 
181
- This makes a fresh npm-installed CLI immediately useful to an agentic workflow.
188
+ This is the seeded-problem path.
189
+
190
+ ## Pull Contract
191
+
192
+ `erdos pull problem <id>` should create a broader workspace-ready bundle containing:
193
+
194
+ - upstream record snapshot for that problem when available
195
+ - generated artifact index for agent consumption
196
+ - seeded local dossier files too when the problem already exists in `problems/<id>/`
197
+ - optional live site snapshot and extracted text when `--include-site` is used
198
+
199
+ This makes a fresh npm-installed CLI immediately useful to an agentic workflow even for problems that are not yet fully seeded as local dossiers.
@@ -209,6 +209,7 @@ And also:
209
209
  - gates
210
210
  - atoms
211
211
  - ready queue
212
+ - compute lane awareness
212
213
  - generated checkpoints
213
214
  - literature mapping
214
215
 
@@ -240,8 +241,18 @@ erdos sunflower setup 857
240
241
  erdos sunflower warnings 857
241
242
  erdos sunflower pass 857
242
243
  erdos sunflower frontier 857
244
+ erdos sunflower status 857
243
245
  ```
244
246
 
247
+ `erdos sunflower status` should be the public-shell place where route state and
248
+ compute posture meet:
249
+
250
+ - active route
251
+ - breakthrough state
252
+ - packaged compute lane
253
+ - next compute action
254
+ - whether paid approval is required
255
+
245
256
  ## Content Policy
246
257
 
247
258
  The repo should not blindly mirror `erdosproblems.com` text.
@@ -281,4 +292,3 @@ This keeps the repo publishable and reduces licensing ambiguity.
281
292
  The first public promise should be:
282
293
 
283
294
  > `erdos-problems` is a CLI atlas and staged research harness for Paul Erdős problems, with the sunflower family as the first deeply integrated pack.
284
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erdos-problems",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI atlas and staged research harness for Paul Erdos problems.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,26 @@
1
+ lane_id: m8_exactness_cube_and_certificate_v0
2
+ problem_id: "857"
3
+ cluster: sunflower
4
+ question: "Is M(8,3) = 45 exact, equivalently is the target-46 SAT instance UNSAT?"
5
+ claim_level_goal: Exact
6
+ status: ready_for_local_scout
7
+ price_checked_local_date: "2026-03-25"
8
+ recommendation: cpu_first
9
+ approval_required: true
10
+ summary: "Packaged sunflower compute lane for the first exact M(8,3) certification packet. The next honest move is the local scout, not paid launch."
11
+ source_repo:
12
+ name: sunflower-coda
13
+ canonical_packet_path: analysis/compute/m8_exactness_cube_and_certificate_v0/launch_packet.yaml
14
+ public_feature:
15
+ command: erdos sunflower status 857
16
+ scaffold_subdir: COMPUTE
17
+ rungs:
18
+ - name: local_scout
19
+ role: local_unmetered
20
+ note: "Run the local scout packet first and require clean artifacts before any paid rung."
21
+ - name: cpu_transfer
22
+ role: paid_metered
23
+ note: "Preferred first paid rung once the local scout has earned promotion."
24
+ - name: heuristic_gpu_sidecar
25
+ role: paid_metered
26
+ note: "Optional and heuristic-only; it supports the exact lane but does not upgrade claims by itself."
package/src/cli/index.js CHANGED
@@ -2,7 +2,9 @@ import { runBootstrapCommand } from '../commands/bootstrap.js';
2
2
  import { runClusterCommand } from '../commands/cluster.js';
3
3
  import { runDossierCommand } from '../commands/dossier.js';
4
4
  import { runProblemCommand } from '../commands/problem.js';
5
+ import { runPullCommand } from '../commands/pull.js';
5
6
  import { runScaffoldCommand } from '../commands/scaffold.js';
7
+ import { runSunflowerCommand } from '../commands/sunflower.js';
6
8
  import { runUpstreamCommand } from '../commands/upstream.js';
7
9
  import { runWorkspaceCommand } from '../commands/workspace.js';
8
10
 
@@ -18,12 +20,14 @@ function printUsage() {
18
20
  console.log(' erdos cluster list');
19
21
  console.log(' erdos cluster show <name>');
20
22
  console.log(' erdos workspace show');
23
+ console.log(' erdos sunflower status [<id>] [--json]');
21
24
  console.log(' erdos dossier show <id>');
22
25
  console.log(' erdos upstream show');
23
26
  console.log(' erdos upstream sync [--write-package-snapshot]');
24
27
  console.log(' erdos upstream diff [--write-package-report]');
25
28
  console.log(' erdos scaffold problem <id> [--dest <path>]');
26
29
  console.log(' erdos bootstrap problem <id> [--dest <path>] [--sync-upstream]');
30
+ console.log(' erdos pull problem <id> [--dest <path>] [--include-site] [--refresh-upstream]');
27
31
  }
28
32
 
29
33
  const args = process.argv.slice(2);
@@ -39,6 +43,8 @@ if (!command || command === 'help' || command === '--help') {
39
43
  exitCode = runClusterCommand(rest);
40
44
  } else if (command === 'workspace') {
41
45
  exitCode = runWorkspaceCommand(rest);
46
+ } else if (command === 'sunflower') {
47
+ exitCode = runSunflowerCommand(rest);
42
48
  } else if (command === 'dossier') {
43
49
  exitCode = runDossierCommand(rest);
44
50
  } else if (command === 'upstream') {
@@ -47,6 +53,8 @@ if (!command || command === 'help' || command === '--help') {
47
53
  exitCode = runScaffoldCommand(rest);
48
54
  } else if (command === 'bootstrap') {
49
55
  exitCode = await runBootstrapCommand(rest);
56
+ } else if (command === 'pull') {
57
+ exitCode = await runPullCommand(rest);
50
58
  } else {
51
59
  console.error(`Unknown command: ${command}`);
52
60
  printUsage();
@@ -129,6 +129,12 @@ function printArtifactInventory(problem, inventory, asJson) {
129
129
  if (inventory.packContext) {
130
130
  console.log(`- ${inventory.packContext.label}: ${inventory.packContext.exists ? 'present' : 'missing'} (${inventory.packContext.path})`);
131
131
  }
132
+ if (inventory.computePackets.length > 0) {
133
+ console.log('Compute packets:');
134
+ for (const packet of inventory.computePackets) {
135
+ console.log(`- ${packet.label}: ${packet.exists ? 'present' : 'missing'} (${packet.path}) [${packet.status}]`);
136
+ }
137
+ }
132
138
  if (inventory.upstreamSnapshot) {
133
139
  console.log('Upstream snapshot:');
134
140
  console.log(`- kind: ${inventory.upstreamSnapshot.kind}`);
@@ -0,0 +1,203 @@
1
+ import path from 'node:path';
2
+ import { getProblem } from '../atlas/catalog.js';
3
+ import { ensureDir, writeJson, writeText } from '../runtime/files.js';
4
+ import { getWorkspaceProblemPullDir } from '../runtime/paths.js';
5
+ import { scaffoldProblem } from '../runtime/problem-artifacts.js';
6
+ import { loadActiveUpstreamSnapshot, syncUpstream } from '../upstream/sync.js';
7
+ import { fetchProblemSiteSnapshot } from '../upstream/site.js';
8
+
9
+ function parsePullArgs(args) {
10
+ const [kind, value, ...rest] = args;
11
+ if (kind !== 'problem') {
12
+ return { error: 'Only `erdos pull problem <id>` is supported right now.' };
13
+ }
14
+
15
+ let destination = null;
16
+ let includeSite = false;
17
+ let refreshUpstream = false;
18
+
19
+ for (let index = 0; index < rest.length; index += 1) {
20
+ const token = rest[index];
21
+ if (token === '--dest') {
22
+ destination = rest[index + 1];
23
+ if (!destination) {
24
+ return { error: 'Missing destination path after --dest.' };
25
+ }
26
+ index += 1;
27
+ continue;
28
+ }
29
+ if (token === '--include-site') {
30
+ includeSite = true;
31
+ continue;
32
+ }
33
+ if (token === '--refresh-upstream') {
34
+ refreshUpstream = true;
35
+ continue;
36
+ }
37
+ return { error: `Unknown pull option: ${token}` };
38
+ }
39
+
40
+ return {
41
+ problemId: value,
42
+ destination,
43
+ includeSite,
44
+ refreshUpstream,
45
+ };
46
+ }
47
+
48
+ function writeUpstreamOnlyBundle(problemId, destination, upstreamRecord, snapshot) {
49
+ ensureDir(destination);
50
+
51
+ if (upstreamRecord) {
52
+ writeJson(path.join(destination, 'UPSTREAM_RECORD.json'), upstreamRecord);
53
+ }
54
+
55
+ const generatedAt = new Date().toISOString();
56
+ writeJson(path.join(destination, 'PROBLEM.json'), {
57
+ generatedAt,
58
+ problemId,
59
+ title: `Erdos Problem #${problemId}`,
60
+ cluster: null,
61
+ siteStatus: upstreamRecord?.status?.state ?? 'unknown',
62
+ repoStatus: 'upstream-only',
63
+ harnessDepth: 'unseeded',
64
+ sourceUrl: `https://www.erdosproblems.com/${problemId}`,
65
+ activeRoute: null,
66
+ });
67
+
68
+ writeJson(path.join(destination, 'ARTIFACT_INDEX.json'), {
69
+ generatedAt,
70
+ problemId,
71
+ copiedArtifacts: [],
72
+ canonicalArtifacts: [],
73
+ upstreamSnapshot: snapshot
74
+ ? {
75
+ kind: snapshot.kind,
76
+ manifestPath: snapshot.manifestPath,
77
+ indexPath: snapshot.indexPath,
78
+ yamlPath: snapshot.yamlPath,
79
+ upstreamCommit: snapshot.manifest.upstream_commit ?? null,
80
+ fetchedAt: snapshot.manifest.fetched_at,
81
+ }
82
+ : null,
83
+ includedUpstreamRecord: Boolean(upstreamRecord),
84
+ });
85
+
86
+ writeText(
87
+ path.join(destination, 'README.md'),
88
+ [
89
+ `# Erdos Problem ${problemId} Pull Bundle`,
90
+ '',
91
+ 'This bundle was generated from upstream public metadata.',
92
+ '',
93
+ `- Source: https://www.erdosproblems.com/${problemId}`,
94
+ `- Upstream record included: ${upstreamRecord ? 'yes' : 'no'}`,
95
+ '',
96
+ 'This problem is not yet seeded locally as a canonical dossier in this package.',
97
+ '',
98
+ ].join('\n'),
99
+ );
100
+ }
101
+
102
+ async function maybeWriteSiteBundle(problemId, destination, includeSite) {
103
+ if (!includeSite) {
104
+ return { attempted: false, included: false, error: null };
105
+ }
106
+
107
+ try {
108
+ const siteSnapshot = await fetchProblemSiteSnapshot(problemId);
109
+ writeText(path.join(destination, 'SITE_SNAPSHOT.html'), siteSnapshot.html);
110
+ writeText(path.join(destination, 'SITE_EXTRACT.txt'), siteSnapshot.text);
111
+ writeJson(path.join(destination, 'SITE_EXTRACT.json'), {
112
+ url: siteSnapshot.url,
113
+ fetchedAt: siteSnapshot.fetchedAt,
114
+ title: siteSnapshot.title,
115
+ previewLines: siteSnapshot.previewLines,
116
+ });
117
+ writeText(
118
+ path.join(destination, 'SITE_SUMMARY.md'),
119
+ [
120
+ `# Erdős Problem #${problemId} Site Summary`,
121
+ '',
122
+ `Source: ${siteSnapshot.url}`,
123
+ `Fetched at: ${siteSnapshot.fetchedAt}`,
124
+ `Title: ${siteSnapshot.title}`,
125
+ '',
126
+ '## Preview',
127
+ '',
128
+ ...siteSnapshot.previewLines.map((line) => `- ${line}`),
129
+ '',
130
+ ].join('\n'),
131
+ );
132
+ return { attempted: true, included: true, error: null };
133
+ } catch (error) {
134
+ writeText(path.join(destination, 'SITE_FETCH_ERROR.txt'), String(error.message ?? error));
135
+ return { attempted: true, included: false, error: String(error.message ?? error) };
136
+ }
137
+ }
138
+
139
+ export async function runPullCommand(args) {
140
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
141
+ console.log('Usage:');
142
+ console.log(' erdos pull problem <id> [--dest <path>] [--include-site] [--refresh-upstream]');
143
+ return 0;
144
+ }
145
+
146
+ const parsed = parsePullArgs(args);
147
+ if (parsed.error) {
148
+ console.error(parsed.error);
149
+ return 1;
150
+ }
151
+ if (!parsed.problemId) {
152
+ console.error('Missing problem id.');
153
+ return 1;
154
+ }
155
+
156
+ if (parsed.refreshUpstream) {
157
+ await syncUpstream();
158
+ }
159
+
160
+ const localProblem = getProblem(parsed.problemId);
161
+ const snapshot = loadActiveUpstreamSnapshot();
162
+ const upstreamRecord = snapshot?.index?.by_number?.[String(parsed.problemId)] ?? null;
163
+
164
+ if (!localProblem && !upstreamRecord) {
165
+ console.error(`Problem ${parsed.problemId} is not present in the local dossier set or upstream snapshot.`);
166
+ return 1;
167
+ }
168
+
169
+ const destination = parsed.destination
170
+ ? path.resolve(parsed.destination)
171
+ : getWorkspaceProblemPullDir(parsed.problemId);
172
+
173
+ let scaffoldResult = null;
174
+ if (localProblem) {
175
+ scaffoldResult = scaffoldProblem(localProblem, destination);
176
+ } else {
177
+ writeUpstreamOnlyBundle(String(parsed.problemId), destination, upstreamRecord, snapshot);
178
+ }
179
+
180
+
181
+ const siteStatus = await maybeWriteSiteBundle(String(parsed.problemId), destination, parsed.includeSite);
182
+
183
+ writeJson(path.join(destination, 'PULL_STATUS.json'), {
184
+ generatedAt: new Date().toISOString(),
185
+ problemId: String(parsed.problemId),
186
+ usedLocalDossier: Boolean(localProblem),
187
+ includedUpstreamRecord: Boolean(upstreamRecord),
188
+ upstreamSnapshotKind: snapshot?.kind ?? null,
189
+ siteSnapshotAttempted: siteStatus.attempted,
190
+ siteSnapshotIncluded: siteStatus.included,
191
+ siteSnapshotError: siteStatus.error,
192
+ scaffoldArtifactsCopied: scaffoldResult?.copiedArtifacts.length ?? 0,
193
+ });
194
+
195
+ console.log(`Pull bundle created: ${destination}`);
196
+ console.log(`Local canonical dossier included: ${localProblem ? 'yes' : 'no'}`);
197
+ console.log(`Upstream record included: ${upstreamRecord ? 'yes' : 'no'}`);
198
+ console.log(`Live site snapshot included: ${siteStatus.included ? 'yes' : 'no'}`);
199
+ if (siteStatus.error) {
200
+ console.log(`Live site snapshot note: ${siteStatus.error}`);
201
+ }
202
+ return 0;
203
+ }
@@ -0,0 +1,97 @@
1
+ import { getProblem } from '../atlas/catalog.js';
2
+ import { getWorkspaceRoot } from '../runtime/paths.js';
3
+ import { buildSunflowerStatusSnapshot, writeSunflowerStatusRecord } from '../runtime/sunflower.js';
4
+ import { readCurrentProblem } from '../runtime/workspace.js';
5
+
6
+ function parseStatusArgs(args) {
7
+ const parsed = {
8
+ problemId: null,
9
+ asJson: false,
10
+ };
11
+
12
+ for (let index = 0; index < args.length; index += 1) {
13
+ const token = args[index];
14
+ if (token === '--json') {
15
+ parsed.asJson = true;
16
+ continue;
17
+ }
18
+ if (!parsed.problemId) {
19
+ parsed.problemId = token;
20
+ continue;
21
+ }
22
+ return { error: `Unknown sunflower status option: ${token}` };
23
+ }
24
+
25
+ return parsed;
26
+ }
27
+
28
+ function printSunflowerStatus(snapshot, registryPaths) {
29
+ console.log(`${snapshot.displayName} sunflower harness`);
30
+ console.log(`Title: ${snapshot.title}`);
31
+ console.log(`Active route: ${snapshot.activeRoute ?? '(none)'}`);
32
+ console.log(`Route breakthrough: ${snapshot.routeBreakthrough ? 'yes' : 'no'}`);
33
+ console.log(`Open problem: ${snapshot.openProblem ? 'yes' : 'no'}`);
34
+ console.log(`Problem solved: ${snapshot.problemSolved ? 'yes' : 'no'}`);
35
+ console.log(`Compute lane present: ${snapshot.computeLanePresent ? 'yes' : 'no'}`);
36
+ console.log(`Compute lane count: ${snapshot.computeLaneCount}`);
37
+ console.log(`Compute summary: ${snapshot.computeSummary}`);
38
+ console.log(`Compute next: ${snapshot.computeNextAction}`);
39
+ if (snapshot.activePacket) {
40
+ console.log(`Compute lane: ${snapshot.activePacket.laneId} [${snapshot.activePacket.status}]`);
41
+ console.log(`Claim level goal: ${snapshot.activePacket.claimLevelGoal}`);
42
+ console.log(`Recommendation: ${snapshot.activePacket.recommendation || '(none)'}`);
43
+ console.log(`Approval required: ${snapshot.activePacket.approvalRequired ? 'yes' : 'no'}`);
44
+ console.log(`Price checked: ${snapshot.activePacket.priceCheckedLocalDate || '(unknown)'}`);
45
+ console.log(`Packet file: ${snapshot.activePacket.packetFileName}`);
46
+ }
47
+ console.log(`Registry record: ${registryPaths.latestPath}`);
48
+ }
49
+
50
+ export function runSunflowerCommand(args) {
51
+ const [subcommand, ...rest] = args;
52
+
53
+ if (!subcommand || subcommand === 'help' || subcommand === '--help') {
54
+ console.log('Usage:');
55
+ console.log(' erdos sunflower status [<id>] [--json]');
56
+ return 0;
57
+ }
58
+
59
+ if (subcommand !== 'status') {
60
+ console.error(`Unknown sunflower subcommand: ${subcommand}`);
61
+ return 1;
62
+ }
63
+
64
+ const parsed = parseStatusArgs(rest);
65
+ if (parsed.error) {
66
+ console.error(parsed.error);
67
+ return 1;
68
+ }
69
+
70
+ const problemId = parsed.problemId ?? readCurrentProblem();
71
+ if (!problemId) {
72
+ console.error('Missing problem id and no active problem is selected.');
73
+ return 1;
74
+ }
75
+
76
+ const problem = getProblem(problemId);
77
+ if (!problem) {
78
+ console.error(`Unknown problem: ${problemId}`);
79
+ return 1;
80
+ }
81
+
82
+ if (problem.cluster !== 'sunflower') {
83
+ console.error(`Problem ${problem.problemId} is not in the sunflower harness.`);
84
+ return 1;
85
+ }
86
+
87
+ const snapshot = buildSunflowerStatusSnapshot(problem);
88
+ const registryPaths = writeSunflowerStatusRecord(problem, snapshot, getWorkspaceRoot());
89
+
90
+ if (parsed.asJson) {
91
+ console.log(JSON.stringify({ ...snapshot, registryPaths }, null, 2));
92
+ return 0;
93
+ }
94
+
95
+ printSunflowerStatus(snapshot, registryPaths);
96
+ return 0;
97
+ }
@@ -1,3 +1,5 @@
1
+ import { getProblem } from '../atlas/catalog.js';
2
+ import { buildSunflowerStatusSnapshot } from '../runtime/sunflower.js';
1
3
  import { getWorkspaceSummary } from '../runtime/workspace.js';
2
4
 
3
5
  export function runWorkspaceCommand(args) {
@@ -21,6 +23,19 @@ export function runWorkspaceCommand(args) {
21
23
  console.log(`Active problem: ${summary.activeProblem ?? '(none)'}`);
22
24
  console.log(`Workspace upstream dir: ${summary.upstreamDir}`);
23
25
  console.log(`Workspace scaffold dir: ${summary.scaffoldDir}`);
26
+ console.log(`Workspace pull dir: ${summary.pullDir}`);
24
27
  console.log(`Updated at: ${summary.updatedAt ?? '(none)'}`);
28
+ if (summary.activeProblem) {
29
+ const problem = getProblem(summary.activeProblem);
30
+ if (problem?.cluster === 'sunflower') {
31
+ const sunflower = buildSunflowerStatusSnapshot(problem);
32
+ console.log(`Sunflower route: ${sunflower.activeRoute ?? '(none)'}`);
33
+ console.log(`Sunflower compute: ${sunflower.computeLanePresent ? 'yes' : 'no'}`);
34
+ if (sunflower.activePacket) {
35
+ console.log(`Sunflower compute lane: ${sunflower.activePacket.laneId} [${sunflower.activePacket.status}]`);
36
+ }
37
+ console.log(`Sunflower compute next: ${sunflower.computeNextAction}`);
38
+ }
39
+ }
25
40
  return 0;
26
41
  }
@@ -12,6 +12,14 @@ export function getWorkspaceDir() {
12
12
  return path.join(getWorkspaceRoot(), '.erdos');
13
13
  }
14
14
 
15
+ export function getWorkspaceRegistryDir(workspaceRoot = getWorkspaceRoot()) {
16
+ return path.join(workspaceRoot, '.erdos', 'registry');
17
+ }
18
+
19
+ export function getWorkspaceComputeRegistryDir(workspaceRoot = getWorkspaceRoot()) {
20
+ return path.join(getWorkspaceRegistryDir(workspaceRoot), 'compute');
21
+ }
22
+
15
23
  export function getWorkspaceStatePath() {
16
24
  return path.join(getWorkspaceDir(), 'state.json');
17
25
  }
@@ -52,6 +60,14 @@ export function getWorkspaceProblemScaffoldDir(problemId) {
52
60
  return path.join(getWorkspaceScaffoldsDir(), String(problemId));
53
61
  }
54
62
 
63
+ export function getWorkspacePullsDir() {
64
+ return path.join(getWorkspaceDir(), 'pulls');
65
+ }
66
+
67
+ export function getWorkspaceProblemPullDir(problemId) {
68
+ return path.join(getWorkspacePullsDir(), String(problemId));
69
+ }
70
+
55
71
  export function getProblemDir(problemId) {
56
72
  return path.join(repoRoot, 'problems', String(problemId));
57
73
  }
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { loadActiveUpstreamSnapshot } from '../upstream/sync.js';
4
4
  import { copyFileIfPresent, ensureDir, writeJson, writeText } from './files.js';
5
5
  import { getPackDir } from './paths.js';
6
+ import { listSunflowerComputePackets } from './sunflower.js';
6
7
 
7
8
  const DOSSIER_FILES = [
8
9
  ['problem.yaml', 'problemYamlPath', 'problem.yaml'],
@@ -42,6 +43,15 @@ export function getProblemArtifactInventory(problem) {
42
43
  }
43
44
  : null;
44
45
 
46
+ const computePackets = listSunflowerComputePackets(problem.problemId).map((packet) => ({
47
+ label: packet.laneId || packet.packetFileName,
48
+ path: packet.packetPath,
49
+ destinationName: packet.packetFileName,
50
+ exists: fs.existsSync(packet.packetPath),
51
+ status: packet.status,
52
+ claimLevelGoal: packet.claimLevelGoal,
53
+ }));
54
+
45
55
  return {
46
56
  generatedAt: new Date().toISOString(),
47
57
  problemId: problem.problemId,
@@ -54,6 +64,7 @@ export function getProblemArtifactInventory(problem) {
54
64
  problemDir: problem.problemDir,
55
65
  canonicalArtifacts,
56
66
  packContext,
67
+ computePackets,
57
68
  upstreamSnapshot: snapshot
58
69
  ? {
59
70
  kind: snapshot.kind,
@@ -96,6 +107,24 @@ export function scaffoldProblem(problem, destination) {
96
107
  }
97
108
  }
98
109
 
110
+ const computeArtifacts = [];
111
+ const computeDir = path.join(destination, 'COMPUTE');
112
+ for (const packet of inventory.computePackets) {
113
+ if (!packet.exists) {
114
+ continue;
115
+ }
116
+ const destinationPath = path.join(computeDir, packet.destinationName);
117
+ if (copyFileIfPresent(packet.path, destinationPath)) {
118
+ computeArtifacts.push({
119
+ label: packet.label,
120
+ sourcePath: packet.path,
121
+ destinationPath,
122
+ status: packet.status,
123
+ claimLevelGoal: packet.claimLevelGoal,
124
+ });
125
+ }
126
+ }
127
+
99
128
  if (inventory.upstreamRecord) {
100
129
  writeJson(path.join(destination, 'UPSTREAM_RECORD.json'), inventory.upstreamRecord);
101
130
  }
@@ -117,8 +146,10 @@ export function scaffoldProblem(problem, destination) {
117
146
  problemId: problem.problemId,
118
147
  bundledProblemDir: problem.problemDir,
119
148
  copiedArtifacts,
149
+ computeArtifacts,
120
150
  packContext: inventory.packContext,
121
151
  canonicalArtifacts: inventory.canonicalArtifacts,
152
+ computePackets: inventory.computePackets,
122
153
  upstreamSnapshot: inventory.upstreamSnapshot,
123
154
  includedUpstreamRecord: inventory.upstreamRecordIncluded,
124
155
  };
@@ -138,6 +169,7 @@ export function scaffoldProblem(problem, destination) {
138
169
  `- Repo status: ${problem.repoStatus}`,
139
170
  `- Harness depth: ${problem.harnessDepth}`,
140
171
  `- Upstream record included: ${inventory.upstreamRecordIncluded ? 'yes' : 'no'}`,
172
+ `- Compute packets copied: ${computeArtifacts.length}`,
141
173
  '',
142
174
  ].join('\n'),
143
175
  );
@@ -0,0 +1,208 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parse } from 'yaml';
4
+ import { writeJson } from './files.js';
5
+ import { getPackDir, getWorkspaceComputeRegistryDir } from './paths.js';
6
+
7
+ const CLAIM_LEVEL_PRIORITY = {
8
+ Exact: 4,
9
+ Verified: 3,
10
+ Heuristic: 2,
11
+ Conjecture: 1,
12
+ };
13
+
14
+ const STATUS_PRIORITY = {
15
+ paid_active: 7,
16
+ local_scout_running: 6,
17
+ ready_for_paid_transfer: 5,
18
+ ready_for_local_scout: 4,
19
+ active: 3,
20
+ blocked: 2,
21
+ complete: 1,
22
+ unknown: 0,
23
+ };
24
+
25
+ function getSunflowerComputeDir(problemId) {
26
+ return path.join(getPackDir('sunflower'), 'compute', String(problemId));
27
+ }
28
+
29
+ function readComputePacket(packetPath) {
30
+ const parsed = parse(fs.readFileSync(packetPath, 'utf8')) ?? {};
31
+ return {
32
+ laneId: String(parsed.lane_id ?? '').trim(),
33
+ problemId: String(parsed.problem_id ?? '').trim(),
34
+ cluster: String(parsed.cluster ?? 'sunflower').trim(),
35
+ question: String(parsed.question ?? '').trim(),
36
+ claimLevelGoal: String(parsed.claim_level_goal ?? '').trim(),
37
+ status: String(parsed.status ?? 'unknown').trim() || 'unknown',
38
+ priceCheckedLocalDate: String(parsed.price_checked_local_date ?? '').trim(),
39
+ recommendation: String(parsed.recommendation ?? '').trim(),
40
+ approvalRequired: Boolean(parsed.approval_required),
41
+ summary: String(parsed.summary ?? '').trim(),
42
+ packetPath,
43
+ packetFileName: path.basename(packetPath),
44
+ sourceRepo: parsed.source_repo ?? null,
45
+ publicFeature: parsed.public_feature ?? null,
46
+ rungs: Array.isArray(parsed.rungs) ? parsed.rungs : [],
47
+ };
48
+ }
49
+
50
+ export function listSunflowerComputePackets(problemId) {
51
+ const computeDir = getSunflowerComputeDir(problemId);
52
+ if (!fs.existsSync(computeDir)) {
53
+ return [];
54
+ }
55
+
56
+ return fs
57
+ .readdirSync(computeDir)
58
+ .filter((entry) => entry.endsWith('.yaml') || entry.endsWith('.yml'))
59
+ .sort()
60
+ .map((entry) => readComputePacket(path.join(computeDir, entry)));
61
+ }
62
+
63
+ function chooseActivePacket(packets) {
64
+ if (packets.length === 0) {
65
+ return null;
66
+ }
67
+
68
+ const ranked = [...packets].sort((left, right) => {
69
+ const statusDelta =
70
+ (STATUS_PRIORITY[right.status] ?? STATUS_PRIORITY.unknown)
71
+ - (STATUS_PRIORITY[left.status] ?? STATUS_PRIORITY.unknown);
72
+ if (statusDelta !== 0) {
73
+ return statusDelta;
74
+ }
75
+ const claimDelta =
76
+ (CLAIM_LEVEL_PRIORITY[right.claimLevelGoal] ?? 0)
77
+ - (CLAIM_LEVEL_PRIORITY[left.claimLevelGoal] ?? 0);
78
+ if (claimDelta !== 0) {
79
+ return claimDelta;
80
+ }
81
+ return right.laneId.localeCompare(left.laneId);
82
+ });
83
+
84
+ return ranked[0];
85
+ }
86
+
87
+ function deriveSummary(packet) {
88
+ if (!packet) {
89
+ return {
90
+ computeSummary: 'No packaged compute lane is registered for this sunflower problem yet.',
91
+ computeNextAction: 'Stay in the atlas/dossier lane until a frozen benchmark and artifact bundle exist.',
92
+ budgetState: 'not_applicable',
93
+ };
94
+ }
95
+
96
+ const budgetState = packet.approvalRequired ? 'approval_required' : 'not_required';
97
+ if (packet.summary) {
98
+ return {
99
+ computeSummary: packet.summary,
100
+ computeNextAction:
101
+ packet.status === 'ready_for_local_scout'
102
+ ? 'Run the local scout first, then decide whether paid compute is honestly earned.'
103
+ : packet.status === 'ready_for_paid_transfer'
104
+ ? 'If budget approval is granted, launch the preferred paid rung and mirror the artifacts back into the workspace.'
105
+ : packet.status === 'paid_active'
106
+ ? 'Let the active metered run finish and pull back logs, artifacts, and impact notes before upgrading any claim.'
107
+ : packet.status === 'complete'
108
+ ? 'Review the completed artifact bundle and decide whether the next move is replay, certificate assembly, or a new frozen lane.'
109
+ : 'Refresh the compute lane status before making it part of the next route decision.',
110
+ budgetState,
111
+ };
112
+ }
113
+
114
+ if (packet.status === 'ready_for_local_scout') {
115
+ return {
116
+ computeSummary: `${packet.laneId} is packaged and ready for its local scout.`,
117
+ computeNextAction: 'Run the local scout first, then decide whether paid compute is honestly earned.',
118
+ budgetState,
119
+ };
120
+ }
121
+
122
+ if (packet.status === 'ready_for_paid_transfer') {
123
+ return {
124
+ computeSummary: `${packet.laneId} cleared the scout and is ready for the ${packet.recommendation || 'paid'} rung.`,
125
+ computeNextAction: 'If budget approval is granted, launch the preferred paid rung and mirror the artifacts back into the workspace.',
126
+ budgetState,
127
+ };
128
+ }
129
+
130
+ if (packet.status === 'paid_active') {
131
+ return {
132
+ computeSummary: `${packet.laneId} is actively using metered compute.`,
133
+ computeNextAction: 'Let the active metered run finish and pull back logs, artifacts, and impact notes before upgrading any claim.',
134
+ budgetState,
135
+ };
136
+ }
137
+
138
+ if (packet.status === 'complete') {
139
+ return {
140
+ computeSummary: `${packet.laneId} has a completed compute packet.`,
141
+ computeNextAction: 'Review the completed artifact bundle and decide whether the next move is replay, certificate assembly, or a new frozen lane.',
142
+ budgetState,
143
+ };
144
+ }
145
+
146
+ return {
147
+ computeSummary: `${packet.laneId} is packaged with status ${packet.status}.`,
148
+ computeNextAction: 'Refresh the compute lane status before making it part of the next route decision.',
149
+ budgetState,
150
+ };
151
+ }
152
+
153
+ function compactPacket(packet) {
154
+ if (!packet) {
155
+ return null;
156
+ }
157
+
158
+ return {
159
+ laneId: packet.laneId,
160
+ status: packet.status,
161
+ claimLevelGoal: packet.claimLevelGoal,
162
+ question: packet.question,
163
+ recommendation: packet.recommendation,
164
+ approvalRequired: packet.approvalRequired,
165
+ priceCheckedLocalDate: packet.priceCheckedLocalDate,
166
+ packetFileName: packet.packetFileName,
167
+ sourceRepo: packet.sourceRepo,
168
+ };
169
+ }
170
+
171
+ export function buildSunflowerStatusSnapshot(problem) {
172
+ const packets = listSunflowerComputePackets(problem.problemId);
173
+ const activePacket = chooseActivePacket(packets);
174
+ const summary = deriveSummary(activePacket);
175
+
176
+ return {
177
+ generatedAt: new Date().toISOString(),
178
+ problemId: problem.problemId,
179
+ displayName: problem.displayName,
180
+ title: problem.title,
181
+ cluster: problem.cluster,
182
+ activeRoute: problem.researchState?.active_route ?? null,
183
+ routeBreakthrough: Boolean(problem.researchState?.route_breakthrough),
184
+ openProblem: Boolean(problem.researchState?.open_problem),
185
+ problemSolved: Boolean(problem.researchState?.problem_solved),
186
+ computeLanePresent: Boolean(activePacket),
187
+ computeLaneCount: packets.length,
188
+ computeSummary: summary.computeSummary,
189
+ computeNextAction: summary.computeNextAction,
190
+ budgetState: summary.budgetState,
191
+ activePacket: compactPacket(activePacket),
192
+ computePackets: packets.map((packet) => compactPacket(packet)),
193
+ };
194
+ }
195
+
196
+ export function writeSunflowerStatusRecord(problem, snapshot, workspaceRoot) {
197
+ const registryDir = getWorkspaceComputeRegistryDir(workspaceRoot);
198
+ const timestamp = new Date().toISOString().replaceAll(':', '-');
199
+ const timestampedPath = path.join(registryDir, `${timestamp}__p${problem.problemId}.json`);
200
+ const latestPath = path.join(registryDir, `latest__p${problem.problemId}.json`);
201
+ writeJson(timestampedPath, snapshot);
202
+ writeJson(latestPath, snapshot);
203
+ return {
204
+ registryDir,
205
+ timestampedPath,
206
+ latestPath,
207
+ };
208
+ }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import {
3
3
  getCurrentProblemPath,
4
4
  getWorkspaceDir,
5
+ getWorkspaceProblemPullDir,
5
6
  getWorkspaceProblemScaffoldDir,
6
7
  getWorkspaceRoot,
7
8
  getWorkspaceStatePath,
@@ -66,6 +67,7 @@ export function getWorkspaceSummary() {
66
67
  activeProblem,
67
68
  upstreamDir: getWorkspaceUpstreamDir(),
68
69
  scaffoldDir: activeProblem ? getWorkspaceProblemScaffoldDir(activeProblem) : getWorkspaceProblemScaffoldDir('<problem-id>'),
70
+ pullDir: activeProblem ? getWorkspaceProblemPullDir(activeProblem) : getWorkspaceProblemPullDir('<problem-id>'),
69
71
  updatedAt: state?.updatedAt ?? null,
70
72
  };
71
73
  }
@@ -0,0 +1,80 @@
1
+ const SITE_BASE_URL = 'https://www.erdosproblems.com';
2
+
3
+ function decodeEntities(text) {
4
+ return text
5
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
6
+ .replace(/&#(\d+);/g, (_, decimal) => String.fromCodePoint(Number.parseInt(decimal, 10)))
7
+ .replace(/&nbsp;/g, ' ')
8
+ .replace(/&amp;/g, '&')
9
+ .replace(/&quot;/g, '"')
10
+ .replace(/&#39;/g, "'")
11
+ .replace(/&lt;/g, '<')
12
+ .replace(/&gt;/g, '>');
13
+ }
14
+
15
+ function collapseWhitespace(text) {
16
+ return text.replace(/[ \t]+/g, ' ').replace(/\s*\n\s*/g, '\n').trim();
17
+ }
18
+
19
+ function htmlToReadableText(html) {
20
+ const withoutScripts = html
21
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
22
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ');
23
+ const blockSeparated = withoutScripts
24
+ .replace(/<(br|\/p|\/div|\/li|\/h1|\/h2|\/h3|\/section|\/article|\/tr)>/gi, '\n')
25
+ .replace(/<li[^>]*>/gi, '- ')
26
+ .replace(/<p[^>]*>/gi, '\n')
27
+ .replace(/<div[^>]*>/gi, '\n')
28
+ .replace(/<h[1-6][^>]*>/gi, '\n');
29
+ const stripped = blockSeparated.replace(/<[^>]+>/g, ' ');
30
+ const decoded = decodeEntities(stripped);
31
+ const normalizedLines = decoded
32
+ .split('\n')
33
+ .map((line) => collapseWhitespace(line))
34
+ .filter(Boolean);
35
+ return normalizedLines.join('\n');
36
+ }
37
+
38
+ function extractTitle(html, problemId) {
39
+ const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
40
+ if (!match) {
41
+ return `Erdos Problem #${problemId}`;
42
+ }
43
+ return collapseWhitespace(decodeEntities(match[1]));
44
+ }
45
+
46
+ function selectPreviewLines(lines) {
47
+ const anchorIndex = lines.findIndex((line) => /^(OPEN|SOLVED|PROVED|PARTIAL)\b/i.test(line));
48
+ if (anchorIndex >= 0) {
49
+ return lines.slice(anchorIndex, anchorIndex + 24);
50
+ }
51
+ return lines.slice(0, 24);
52
+ }
53
+
54
+ export async function fetchProblemSiteSnapshot(problemId) {
55
+ const url = `${SITE_BASE_URL}/${problemId}`;
56
+ const response = await fetch(url, {
57
+ headers: {
58
+ 'User-Agent': 'erdos-problems-cli',
59
+ Accept: 'text/html',
60
+ },
61
+ });
62
+
63
+ if (!response.ok) {
64
+ throw new Error(`Unable to fetch problem page ${problemId}: ${response.status}`);
65
+ }
66
+
67
+ const html = await response.text();
68
+ const text = htmlToReadableText(html);
69
+ const title = extractTitle(html, problemId);
70
+ const lines = text.split('\n').filter(Boolean);
71
+
72
+ return {
73
+ url,
74
+ fetchedAt: new Date().toISOString(),
75
+ html,
76
+ title,
77
+ text,
78
+ previewLines: selectPreviewLines(lines),
79
+ };
80
+ }