pi-eda 0.1.0 → 0.2.0

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
@@ -5,6 +5,7 @@ Pi skills and subagents for interactive exploratory data analysis.
5
5
  - `skills/exploring-data`: iterative analysis & plotting
6
6
  - `skills/finishing-exploratory-data-analysis`: locking in analysis into a wiki-style report
7
7
  - `show` extension: renders a figure to the user in the TUI without spending tokens on it
8
+ - `eda-workflow-tracker` extension: tracks stage progress through `exploring-data`'s workflow graph
8
9
 
9
10
  ## Requires
10
11
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: job-runner
3
- description: Launches and monitors a heavy/long-running analysis script in the background
3
+ description: Launches a heavy/long-running analysis script under a memory cap and blocks on it
4
4
  tools: bash, read
5
5
  inheritProjectContext: true
6
6
  ---
@@ -12,23 +12,26 @@ that already happened before you were dispatched.
12
12
  ## What to do
13
13
 
14
14
  - Confirm inputs before launching:
15
- - The estimated runtime and peak memory from the smoke test (should be
16
- in the task you were given -- if missing, ask the dispatching agent
17
- for it rather than guessing).
18
- - Whether `systemd-run` is available on this machine.
19
- - Launch the script as a background process (following the project's run
20
- convention, e.g. `uv run`).
21
- - Cap it with a memory limit so a runaway process gets killed by the OS
22
- instead of taking down the machine, sized off the estimate with
23
- headroom (e.g. 2x the estimated peak):
24
- - `systemd-run --scope -p MemoryMax=<budget> --user -- uv run <script>`
25
- if available.
26
- - Otherwise, say so explicitly and fall back to periodic `ps`/`smem`
27
- checks with a manual kill.
28
- - Monitor progress and memory use periodically rather than blocking on it
29
- synchronously -- this is in addition to the hard cap above, not a
30
- replacement for it, since you want to know about a job trending toward
31
- the ceiling before it gets killed.
15
+ - The estimated runtime (should be in the task you were given -- if
16
+ missing, ask the dispatching agent rather than guessing).
17
+ - Whether `systemd-run` is available (Linux only -- no shell-level
18
+ equivalent on macOS or Windows).
19
+ - Determine how much memory can be used for the process. Check
20
+ available memory with `free -m` and leave at least 1GB free.
21
+ - If `systemd-run` is available:
22
+ - Run in the foreground, blocked on completion using:
23
+ `systemd-run --scope -p MemoryMax=<budget> --user -- uv run <script>`.
24
+ - The cap is the safety net, so you should not:
25
+ - Poll for memory consumption
26
+ - Detach from the process
27
+ - If `systemd-run` is not available:
28
+ - Say so explicitly. There is no OS cap here, so you are the safety
29
+ net.
30
+ - Launch the script as a separate process, then poll memory yourself
31
+ against the memory budget.
32
+ - Do this as a *single* bash call with an internal sleep/check loop.
33
+ Kill and report if it crosses the budget to save token context.
34
+ Do not use separate agent turns here.
32
35
  - Report back once it completes: success/failure, runtime, and where the
33
36
  output landed. On failure, include the actual error, not just "it
34
37
  failed."
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Workflow Tracker Extension
3
+ *
4
+ * A native pi tool for tracking progress through a named-state workflow
5
+ * graph (branches + loop-backs), not a flat todo list. Purely descriptive:
6
+ * nothing downstream enforces it, `advance` just validates the move is
7
+ * legal and remembers where the agent said it is.
8
+ *
9
+ * State is stored in tool result details for proper branching support,
10
+ * same restore-from-branch pattern as plan_tracker. Shows a one-line
11
+ * status widget with the raw current node name -- transparency for the
12
+ * user, not enforcement.
13
+ *
14
+ * Pure logic lives in lib/eda-workflow-tracker-core.ts for testability. Nested
15
+ * one level deep (not top-level extensions/, not an index.ts subdir) so pi's
16
+ * extension auto-discovery does not try to load it as a standalone extension.
17
+ */
18
+
19
+ import { readFile } from "node:fs/promises";
20
+ import { resolve } from "node:path";
21
+ import type {
22
+ ExtensionAPI,
23
+ ExtensionContext,
24
+ } from "@earendil-works/pi-coding-agent";
25
+ import { Text } from "@earendil-works/pi-tui";
26
+ import { Type, type Static } from "typebox";
27
+ import {
28
+ type WorkflowGraph,
29
+ type WorkflowTrackerDetails,
30
+ type BranchEntry,
31
+ type ActionResult,
32
+ isValidGraph,
33
+ handleInit,
34
+ handleAdvance,
35
+ handleStatus,
36
+ handleClear,
37
+ formatWidgetText,
38
+ reconstructFromBranch,
39
+ } from "./lib/eda-workflow-tracker-core.js";
40
+
41
+ const WorkflowTrackerParams = Type.Object({
42
+ action: Type.Union(
43
+ [
44
+ Type.Literal("init"),
45
+ Type.Literal("advance"),
46
+ Type.Literal("status"),
47
+ Type.Literal("clear"),
48
+ ],
49
+ { description: "Action to perform" },
50
+ ),
51
+ graphPath: Type.Optional(
52
+ Type.String({
53
+ description:
54
+ "Path to a workflow.graph.json ({ start, edges }) (for init)",
55
+ }),
56
+ ),
57
+ to: Type.Optional(
58
+ Type.String({
59
+ description: "Node name to advance to (for advance)",
60
+ }),
61
+ ),
62
+ force: Type.Optional(
63
+ Type.Boolean({
64
+ description:
65
+ "Allow advancing to any valid node in the graph, not just ones adjacent to the current node (for advance)",
66
+ }),
67
+ ),
68
+ });
69
+
70
+ export type WorkflowTrackerInput = Static<typeof WorkflowTrackerParams>;
71
+
72
+ export default function (pi: ExtensionAPI) {
73
+ let graphPath = "";
74
+ let graph: WorkflowGraph | undefined;
75
+ let current = "";
76
+
77
+ const loadGraphFile = async (cwd: string, path: string): Promise<unknown> => {
78
+ const raw = await readFile(resolve(cwd, path), "utf-8");
79
+ return JSON.parse(raw);
80
+ };
81
+
82
+ const reconstructState = async (ctx: ExtensionContext) => {
83
+ const state = reconstructFromBranch(
84
+ ctx.sessionManager.getBranch() as BranchEntry[],
85
+ );
86
+ graphPath = state.graphPath;
87
+ current = state.current;
88
+ graph = undefined;
89
+ if (!graphPath) return;
90
+ try {
91
+ const parsed = await loadGraphFile(ctx.cwd, graphPath);
92
+ if (isValidGraph(parsed)) graph = parsed;
93
+ } catch {
94
+ // leave graph undefined -- advance/status report it as not loaded
95
+ }
96
+ };
97
+
98
+ const updateWidget = (ctx: ExtensionContext) => {
99
+ if (!ctx.hasUI) return;
100
+ const text = formatWidgetText(current);
101
+ if (text) {
102
+ ctx.ui.setWidget("eda-workflow-tracker", (_tui, theme) => {
103
+ return new Text(theme.fg("muted", text), 0, 0);
104
+ });
105
+ } else {
106
+ ctx.ui.setWidget("eda-workflow-tracker", undefined);
107
+ }
108
+ };
109
+
110
+ for (const event of [
111
+ "session_start",
112
+ "session_switch",
113
+ "session_fork",
114
+ "session_tree",
115
+ ] as const) {
116
+ pi.on(event, async (_event, ctx) => {
117
+ await reconstructState(ctx);
118
+ updateWidget(ctx);
119
+ });
120
+ }
121
+
122
+ pi.registerTool({
123
+ name: "eda-workflow-tracker",
124
+ label: "Workflow Tracker",
125
+ description:
126
+ "Track the current state in a named-state workflow graph (branches/loop-backs, not a flat " +
127
+ "todo list). Actions: init (load a workflow.graph.json and set current to its start), " +
128
+ "advance (move to a node; must be adjacent to current unless force is set, in which case " +
129
+ "any valid node in the graph is allowed), status (show current state), clear (reset).",
130
+ parameters: WorkflowTrackerParams,
131
+
132
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
133
+ let result: ActionResult;
134
+
135
+ switch (params.action) {
136
+ case "init": {
137
+ if (!params.graphPath) {
138
+ result = {
139
+ graphPath,
140
+ current,
141
+ next: [],
142
+ error: "graphPath required for init",
143
+ };
144
+ break;
145
+ }
146
+ try {
147
+ const parsed = await loadGraphFile(ctx.cwd, params.graphPath);
148
+ if (!isValidGraph(parsed)) {
149
+ throw new Error("expected { start: string, edges: object }");
150
+ }
151
+ graph = parsed;
152
+ } catch (err) {
153
+ result = {
154
+ graphPath,
155
+ current,
156
+ next: [],
157
+ error: `failed to load graph at "${params.graphPath}": ${(err as Error).message}`,
158
+ };
159
+ break;
160
+ }
161
+ result = handleInit(graph, params.graphPath);
162
+ graphPath = result.graphPath;
163
+ current = result.current;
164
+ updateWidget(ctx);
165
+ break;
166
+ }
167
+ case "advance": {
168
+ if (!graph) {
169
+ result = {
170
+ graphPath,
171
+ current,
172
+ next: [],
173
+ error: "no graph loaded -- call init first",
174
+ };
175
+ break;
176
+ }
177
+ if (!params.to) {
178
+ result = {
179
+ graphPath,
180
+ current,
181
+ next: [],
182
+ error: "to required for advance",
183
+ };
184
+ break;
185
+ }
186
+ result = handleAdvance(
187
+ graph,
188
+ current,
189
+ params.to,
190
+ params.force ?? false,
191
+ graphPath,
192
+ );
193
+ current = result.current;
194
+ updateWidget(ctx);
195
+ break;
196
+ }
197
+ case "status": {
198
+ result = handleStatus(graph, current, graphPath);
199
+ break;
200
+ }
201
+ case "clear": {
202
+ result = handleClear();
203
+ graphPath = result.graphPath;
204
+ current = result.current;
205
+ graph = undefined;
206
+ updateWidget(ctx);
207
+ break;
208
+ }
209
+ default: {
210
+ result = {
211
+ graphPath,
212
+ current,
213
+ next: [],
214
+ error: `unknown action: ${params.action as string}`,
215
+ };
216
+ }
217
+ }
218
+
219
+ const details: WorkflowTrackerDetails = {
220
+ action: params.action,
221
+ graphPath: result.graphPath,
222
+ current: result.current,
223
+ next: result.next,
224
+ ...(result.error ? { error: result.error } : {}),
225
+ };
226
+
227
+ return {
228
+ content: [
229
+ {
230
+ type: "text",
231
+ text: result.error
232
+ ? `Error: ${result.error}`
233
+ : `current: "${result.current}"; next: ${JSON.stringify(result.next)}`,
234
+ },
235
+ ],
236
+ details,
237
+ };
238
+ },
239
+
240
+ renderCall(args, theme) {
241
+ let text = theme.fg("toolTitle", theme.bold("eda-workflow-tracker "));
242
+ text += theme.fg("muted", args.action);
243
+ if (args.action === "advance" && args.to) {
244
+ text += ` -> ${theme.fg("accent", args.to)}`;
245
+ if (args.force) text += theme.fg("dim", " (force)");
246
+ }
247
+ return new Text(text, 0, 0);
248
+ },
249
+
250
+ renderResult(result, _options, theme) {
251
+ const details = result.details as WorkflowTrackerDetails | undefined;
252
+ if (!details) {
253
+ const text = result.content[0];
254
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
255
+ }
256
+ if (details.error) {
257
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
258
+ }
259
+ return new Text(
260
+ theme.fg("success", "✓ ") + theme.fg("muted", details.current),
261
+ 0,
262
+ 0,
263
+ );
264
+ },
265
+ });
266
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Workflow Tracker Core Logic
3
+ *
4
+ * Pure functions for a graph-shaped (not list-shaped) progress tracker:
5
+ * "current" is a node name in a caller-supplied edges map, advanced by
6
+ * naming the next node rather than by index. No pi dependencies.
7
+ */
8
+
9
+ export interface WorkflowGraph {
10
+ start: string;
11
+ edges: Record<string, string[]>;
12
+ }
13
+
14
+ /** True if `value` has the { start: string, edges: object } shape a WorkflowGraph requires. */
15
+ export function isValidGraph(value: unknown): value is WorkflowGraph {
16
+ if (typeof value !== "object" || value === null) return false;
17
+ const candidate = value as Record<string, unknown>;
18
+ return (
19
+ typeof candidate.start === "string" &&
20
+ typeof candidate.edges === "object" &&
21
+ candidate.edges !== null
22
+ );
23
+ }
24
+
25
+ export interface WorkflowTrackerDetails {
26
+ action: "init" | "advance" | "status" | "clear";
27
+ graphPath: string;
28
+ current: string;
29
+ next: string[];
30
+ error?: string;
31
+ }
32
+
33
+ export interface ActionResult {
34
+ graphPath: string;
35
+ current: string;
36
+ next: string[];
37
+ error?: string;
38
+ }
39
+
40
+ /** Nodes reachable in one step from `node`, or [] if none/no graph loaded. */
41
+ function nextFrom(graph: WorkflowGraph | undefined, node: string): string[] {
42
+ return graph?.edges[node] ?? [];
43
+ }
44
+
45
+ function allNodes(edges: Record<string, string[]>): Set<string> {
46
+ const nodes = new Set<string>(Object.keys(edges));
47
+ for (const targets of Object.values(edges)) {
48
+ for (const target of targets) nodes.add(target);
49
+ }
50
+ return nodes;
51
+ }
52
+
53
+ /** Case-insensitive lookup: returns the canonically-cased node name, or undefined if none matches. */
54
+ function canonicalize(
55
+ candidates: Iterable<string>,
56
+ name: string,
57
+ ): string | undefined {
58
+ const lower = name.toLowerCase();
59
+ for (const candidate of candidates) {
60
+ if (candidate.toLowerCase() === lower) return candidate;
61
+ }
62
+ return undefined;
63
+ }
64
+
65
+ export function handleInit(
66
+ graph: WorkflowGraph,
67
+ graphPath: string,
68
+ ): ActionResult {
69
+ return {
70
+ graphPath,
71
+ current: graph.start,
72
+ next: nextFrom(graph, graph.start),
73
+ };
74
+ }
75
+
76
+ export function handleAdvance(
77
+ graph: WorkflowGraph,
78
+ current: string,
79
+ to: string,
80
+ force: boolean,
81
+ graphPath = "",
82
+ ): ActionResult {
83
+ const legalFromCurrent = graph.edges[current] ?? [];
84
+
85
+ if (!force) {
86
+ const canonical = canonicalize(legalFromCurrent, to);
87
+ if (!canonical) {
88
+ return {
89
+ graphPath,
90
+ current,
91
+ next: legalFromCurrent,
92
+ error: `"${to}" is not reachable from "${current}"; legal: ${JSON.stringify(legalFromCurrent)}`,
93
+ };
94
+ }
95
+ return { graphPath, current: canonical, next: nextFrom(graph, canonical) };
96
+ }
97
+
98
+ const canonical = canonicalize(allNodes(graph.edges), to);
99
+ if (!canonical) {
100
+ return {
101
+ graphPath,
102
+ current,
103
+ next: legalFromCurrent,
104
+ error: `"${to}" is not a known node in this graph; legal: ${JSON.stringify(legalFromCurrent)}`,
105
+ };
106
+ }
107
+ return { graphPath, current: canonical, next: nextFrom(graph, canonical) };
108
+ }
109
+
110
+ export function handleStatus(
111
+ graph: WorkflowGraph | undefined,
112
+ current: string,
113
+ graphPath = "",
114
+ ): ActionResult {
115
+ return { graphPath, current, next: nextFrom(graph, current) };
116
+ }
117
+
118
+ export function handleClear(): ActionResult {
119
+ return { graphPath: "", current: "", next: [] };
120
+ }
121
+
122
+ export function formatWidgetText(current: string): string | undefined {
123
+ return current ? `EDA workflow: ${current}` : undefined;
124
+ }
125
+
126
+ // --- State Reconstruction ---
127
+
128
+ export interface BranchEntry {
129
+ type: string;
130
+ message?: {
131
+ role: string;
132
+ toolName?: string;
133
+ details?: WorkflowTrackerDetails;
134
+ };
135
+ }
136
+
137
+ export function reconstructFromBranch(entries: BranchEntry[]): ActionResult {
138
+ let state: ActionResult = { graphPath: "", current: "", next: [] };
139
+ for (const entry of entries) {
140
+ if (entry.type !== "message") continue;
141
+ const msg = entry.message;
142
+ if (
143
+ !msg ||
144
+ msg.role !== "toolResult" ||
145
+ msg.toolName !== "eda-workflow-tracker"
146
+ )
147
+ continue;
148
+ const details = msg.details;
149
+ if (details && !details.error) {
150
+ state = {
151
+ graphPath: details.graphPath,
152
+ current: details.current,
153
+ next: details.next,
154
+ };
155
+ }
156
+ }
157
+ return state;
158
+ }
package/package.json CHANGED
@@ -1,10 +1,19 @@
1
1
  {
2
2
  "name": "pi-eda",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Pi tooling for interactive exploratory data analysis.",
5
5
  "license": "MIT",
6
6
  "author": "Shivam V Verma",
7
- "keywords": ["pi-package", "pi", "eda", "exploratory-data-analysis"],
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/shivam-v-verma/pi-eda.git"
10
+ },
11
+ "keywords": [
12
+ "pi-package",
13
+ "pi",
14
+ "eda",
15
+ "exploratory-data-analysis"
16
+ ],
8
17
  "files": [
9
18
  "README.md",
10
19
  "agents",
@@ -12,16 +21,29 @@
12
21
  "extensions",
13
22
  "skills"
14
23
  ],
24
+ "scripts": {
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
27
+ },
15
28
  "peerDependencies": {
16
29
  "@earendil-works/pi-coding-agent": "*",
17
30
  "@earendil-works/pi-tui": "*",
18
31
  "typebox": "*"
19
32
  },
33
+ "devDependencies": {
34
+ "vitest": "^4.1.11"
35
+ },
20
36
  "pi": {
21
- "extensions": ["./extensions"],
22
- "skills": ["./skills"],
37
+ "extensions": [
38
+ "./extensions"
39
+ ],
40
+ "skills": [
41
+ "./skills"
42
+ ],
23
43
  "subagents": {
24
- "agents": ["./agents"]
44
+ "agents": [
45
+ "./agents"
46
+ ]
25
47
  }
26
48
  }
27
49
  }
@@ -23,9 +23,15 @@ and suggest or build the plots that interrogate them. What a confirmed pattern
23
23
  *means*, the story it tells, is the user's call: hand back a clear summary of
24
24
  what the figure shows, not a conclusion dressed as a finding.
25
25
 
26
- See `workflow.md` for stages of this skill. In all stages, confirm framing
27
- with the user. Never run a batch of steps by yourself; always interleave
28
- by checking for the user's insight/thoughts.
26
+ If using this skill, you **must** first:
27
+
28
+ - Run `init` on `eda-workflow-tracker` with `graphPath:
29
+ "skills/exploring-data/workflow.graph.json"` for guidance on what stage you
30
+ should be working on.
31
+ - Read `workflow.md` for instructions for each stage.
32
+
33
+ **Always call `advance` on the `eda-workflow-tracker` when done with a
34
+ stage before performing the next action or moving on to the next stage.**
29
35
 
30
36
  A good hypothesis is usually a simple, clean one -- reachable through better
31
37
  framing of the question, not through aggressively slicing the dataset more ways
@@ -59,10 +65,12 @@ its own narrower table.
59
65
 
60
66
  | Mistake | Fix |
61
67
  | --- | --- |
62
- | Running the full dataset before a smoke test | See `checking-and-running-scripts.md` -- always sample/time on a subset first |
63
- | Auto-advancing to the next question without a framing check | See `pre-code-checkpoints.md` -- confirm framing before each new question, not once for the whole analysis |
68
+ | Doing any work using this skill (e.g. scoping the analysis, peeking at data, building a script) without having initialized `eda-workflow-tracker` with `init` | `init` as soon as you decide to use this skill. `eda-workflow-tracker` is essential to this skill. |
69
+ | Doing a stage without calling `advance` after it | Treat `advance` as part of finishing the stage, not separate bookkeeping. If you just did something on the graph, the next tool call is `advance`, not the action of the next stage |
70
+ | Running the full dataset before a smoke test | See `workflow.md` (`Build script/plot`) -- always sample/time on a subset first |
71
+ | Auto-advancing to the next question without a framing check | See `workflow.md` (`Propose/ask a question`) -- confirm framing before each new question, not once for the whole analysis |
64
72
  | Picking the report/subfolder name yourself instead of asking | Ask before creating it -- one name, shared by the file and its folder |
65
- | Launching a heavy scale-up run while the `script-checker` is still pending | See `checking-and-running-scripts.md` -- wait for it, or the expensive run finishes before a material issue could surface |
73
+ | Launching a heavy scale-up run while the `script-checker` is still pending | See `workflow.md` (`Dispatch script-checker`) -- wait for it, or the expensive run finishes before a material issue could surface |
66
74
  | Deleting/pruning a failed or paused script | Leave it in place with a status docstring unless the user explicitly asks to remove it |
67
75
  | Treating an approval-sounding signal ("looks great", "keep going", "I trust your judgment") as done | Ask whether to move to `/skill:finishing-exploratory-data-analysis` -- don't auto-advance and don't stay silent |
68
76
  | Chasing a pattern that only appears after trying many slices/cuts of the data | Multiple-comparisons risk -- go back to framing, not more cuts |
@@ -0,0 +1,19 @@
1
+ {
2
+ "start": "Scope the analysis",
3
+ "edges": {
4
+ "Scope the analysis": ["Peek at the data"],
5
+ "Peek at the data": ["Propose/ask a question"],
6
+ "Propose/ask a question": ["Build script/plot"],
7
+ "Build script/plot": ["Light or heavy?"],
8
+ "Light or heavy?": ["Reread script", "Dispatch script-checker"],
9
+ "Reread script": ["Build script/plot", "Run now, this session"],
10
+ "Dispatch script-checker": ["Build script/plot", "Dispatch job-runner"],
11
+ "Run now, this session": ["Summarize the answer"],
12
+ "Dispatch job-runner": ["Summarize the answer"],
13
+ "Summarize the answer": ["Continue analysis or lock in?"],
14
+ "Continue analysis or lock in?": [
15
+ "Propose/ask a question",
16
+ "/skill:finishing-exploratory-data-analysis"
17
+ ]
18
+ }
19
+ }
@@ -1,66 +1,184 @@
1
1
  # Workflow
2
2
 
3
- Loop/flow-control for `/skill:exploring-data`.
4
-
5
- The user drives this loop: they propose or pick the next thing to plot or
6
- check, you build it and return what it shows. They then interpret it
7
- and pick next steps or ask additional questions.
8
-
9
- This is a looped pipeline, exiting only when the user decides to
10
- lock-in an analysis.
11
-
12
- ```dot
13
- digraph EDA_WORKFLOW {
14
- "Start analysis" [shape=doublecircle];
15
- "Scope the analysis" [shape=box, style=filled, fillcolor=lightblue];
16
- "Peek at the data" [shape=box, style=filled, fillcolor=lightyellow];
17
- "Propose/ask a question" [shape=box, style=filled, fillcolor=lightblue];
18
- "Build script/plot" [shape=box];
19
- "light or heavy?" [shape=diamond];
20
- "Reread script" [shape=box];
21
- "Dispatch script-checker (blocking)" [shape=box];
22
- "run now, this session" [shape=box];
23
- "Dispatch job-runner" [shape=box];
24
- "Summarize the answer" [shape=box, style=filled, fillcolor=lightyellow];
25
- "Continue analysis or lock in?" [shape=diamond, style=filled, fillcolor=lightblue];
26
- "/skill:finishing-exploratory-data-analysis" [shape=doublecircle];
27
-
28
- "Start analysis" -> "Scope the analysis";
29
- "Scope the analysis" -> "Peek at the data";
30
- "Peek at the data" -> "Propose/ask a question";
31
- "Propose/ask a question" -> "Build script/plot";
32
- "Build script/plot" -> "light or heavy?";
33
- "light or heavy?" -> "Reread script" [label="light"];
34
- "Reread script" -> "Build script/plot" [label="issues found"];
35
- "Reread script" -> "run now, this session" [label="looks right"];
36
- "light or heavy?" -> "Dispatch script-checker (blocking)" [label="heavy"];
37
- "Dispatch script-checker (blocking)" -> "Build script/plot" [label="issues found"];
38
- "Dispatch script-checker (blocking)" -> "Dispatch job-runner" [label="clean"];
39
- "run now, this session" -> "Summarize the answer";
40
- "Dispatch job-runner" -> "Summarize the answer";
41
- "Summarize the answer" -> "Continue analysis or lock in?";
42
- "Continue analysis or lock in?" -> "Propose/ask a question" [label="next question"];
43
- "Continue analysis or lock in?" -> "/skill:finishing-exploratory-data-analysis" [label="finish analysis"];
44
- }
45
- ```
46
-
47
- (blue = User turn, yellow = Agent -> User handoff, white = Agent-only)
48
-
49
- | Stage | Turn | What it looks like | See |
50
- | --- | --- | --- | --- |
51
- | Scope the analysis | User | State the broad scope/question area for this analysis and confirm it, once per analysis, before touching data | `pre-code-checkpoints.md` (Scope the analysis) |
52
- | Peek at the data | Agent -> User | Schema/dtypes, `.head()`, a sample, eyeball raw rows, hand back for a look | `pre-code-checkpoints.md` (Peek at the data) |
53
- | Propose/ask a question | User (agent may suggest) | Confirm framing for the next question, then the user picks what to plot or check | `pre-code-checkpoints.md` (Propose/ask a question) |
54
- | Build script/plot | Agent | Write the script/plot, smoke test on a subset before scaling | `checking-and-running-scripts.md`, `plotting.md` |
55
- | light or heavy? | Agent | Judge from the smoke-test timing/size ratio | `checking-and-running-scripts.md` |
56
- | Reread script | Agent | Light: reread the script yourself before running -- no `script-checker` dispatch, loop back to the script on issues you find | `checking-and-running-scripts.md` |
57
- | Dispatch script-checker (blocking) / Dispatch job-runner | Agent | Heavy: dispatch the `script-checker` and wait; loop back to the script on findings; don't launch the `job-runner` until it's clean | `checking-and-running-scripts.md` |
58
- | Summarize the answer | Agent -> User | Interpret the figure/stats as a summary, not a conclusion -- the user decides what it means and what's next | `SKILL.md` Core principle |
59
- | Lock-in checkpoint | User | Figure/analysis seems meaningful, ready to write up or commit | `/skill:finishing-exploratory-data-analysis` |
3
+ This file is the legend for the EDA workflow.
4
+
5
+ Use the `eda-workflow-tracker` tool to determine what stage you're on; perform
6
+ exactly that stage of work; and then call `advance` before starting the next
7
+ stage. Call `status` whenever you need to know where you are instead of holding
8
+ it in your head.
9
+
10
+ At the end of every turn, ask yourself if you have accomplished the exit
11
+ condition to `advance` to the next stage.
12
+
13
+ - If yes, `advance` should **always** be your first tool call.
14
+ - If no, keep working on the stage until you have reached the exit condition.
15
+
16
+ ## Stages
17
+
18
+ ### `Scope the analysis`
19
+
20
+ - Before opening the data, ask in one or two sentences what is broadly in
21
+ scope for this analysis. This is a "what are we even doing here", at a
22
+ high level.
23
+ - Keep interrogating until the user agrees with the framing. Do not advance
24
+ on a partial answer or lukewarm agreement.
25
+ - Ask the user for a report name for this analysis -- it names both the
26
+ report file and its paired folder (see `directory-structure.md`).
27
+ Don't pick one yourself. Keep asking until they give one.
28
+ - Expect the scope to narrow or shift as the data yields a story during
29
+ this analysis. This step is purely for you to ground yourself.
30
+ - You should **never** be running code at this stage. If the user has
31
+ already offered specific questions/approaches, you must confirm your
32
+ understanding and then `advance`.
33
+ - `advance` condition: the user has confirmed the scope of the analysis and has
34
+ given a report name; or the user has already requested a specific question.
35
+
36
+ ### `Peek at the data`
37
+
38
+ - Load the dataset (lazily), check schema/dtypes.
39
+ - Get null counts and basic summary stats (mean/median, min/max, std).
40
+ - Get a feel for the actual data: `.head()`, a strided sample, or a random
41
+ sample (fixed seed).
42
+ - Read a few raw rows by eye. They catch what schema/dtype checks and basic
43
+ stats above miss: a "numeric" column that's really a formatted string,
44
+ an ID convention, units, or what a free-text field actually holds.
45
+ - Even raw rows and basic stats can miss duplicate rows.
46
+ - Beyond that, think about what else is cheap to calculate and might
47
+ reveal something missed above -- e.g. sentinel/placeholder values (`-1`,
48
+ `0`, `"unknown"`), class imbalance, or a grain mismatch after a join.
49
+ - `advance` condition: you understand the high-level structure of the data.
50
+
51
+ ### `Propose/ask a question`
52
+
53
+ - Propose a framing for *this* question/figure yourself, then keep
54
+ interrogating until the user agrees with it, before writing a script for it.
55
+ - You can refine the approach by writing/executing some code, but don't write
56
+ any scripts in the directory yet. This is exploratory, not polish.
57
+ - The goal here is focus: work with the user to identify a specific, question
58
+ that can be tested with data. Do not attempt many questions or approaches at
59
+ once. Iterate one at a time with the user until it's settled.
60
+ - If the user has already proposed a specific question, confirm your
61
+ understanding vocally and then `advance`.
62
+ - `advance` condition: You have explained what you plan to do, and the user
63
+ agrees with the question and approach.
64
+
65
+ ### `Build script/plot`
66
+
67
+ Goal: Write the script/plot (see `plotting.md` for chart conventions), then
68
+ build confidence in it:
69
+
70
+ - After each nontrivial transform (groupby, join, pivot, filter, etc.), do
71
+ a quick check (`.head()`, row counts, spot-check a few values) that the
72
+ output matches what the transform should have done. Don't just do this once
73
+ at the end of the script.
74
+ - Pick a case that could actually fail, not the first one at hand.
75
+ especially for canonicalization/ordering/matching logic, where an easy
76
+ case can pass while hiding a real bug.
77
+ - Time the script on a small subset of the data to understand how long the
78
+ full scale run will take
79
+ - Glance at the actual output values to sanity check the output.
80
+ - `advance` condition: the script is written and each nontrivial transform has
81
+ been spot-checked.
82
+
83
+ ### `Light or heavy?`
84
+
85
+ Goal: Decide whether the full scale run will be a light task or a heavy task.
86
+ We want to increase iteration speed and not waste user time.
87
+
88
+ - Judge from the smoke-test timing/size ratio whether the full-scale run
89
+ will be heavy (long-running or memory-heavy relative to the sample --
90
+ e.g. 1% took a second but full scale extrapolates to minutes).
91
+ - Rough thresholds based on estimated time: light if under ~1 minute,
92
+ heavy if over ~5 minutes. In between is a judgment call.
93
+ - State that judgment explicitly: "sample took Xs, full run estimated ~Y,
94
+ treating as heavy."
95
+ - `advance` condition: you understand the computational cost of the script.
96
+
97
+ ### `Reread script`
98
+
99
+ Light path: Reread the script as a sanity check.
100
+
101
+ - Does it match the user's stated intent/framing, does each nontrivial
102
+ transform do what it should?
103
+ - If the reread surfaces an issue, fix it and reread again before
104
+ running. Don't run first and raise the concern after.
105
+ - This is a deliberately lower bar than the heavy path. A subtle mistake here
106
+ can also be caught downstream in `/skill:finishing-exploratory-data-analysis`.
107
+ We just want to sanity check here.
108
+ - `advance` condition: you have reread the script and haven't found issues.
109
+
110
+ ### `Run now, this session`
111
+
112
+ Light path continuation: run the smoke-tested script/plot in this session.
113
+
114
+ - `advance` condition: you have run the script successfully on the data.
115
+
116
+ ### `Dispatch script-checker`
117
+
118
+ Heavy path: dispatch the `script-checker` and wait, before launching.
119
+
120
+ - `subagent({agent:"script-checker",
121
+ task:"<script path + stated intent/framing>"})`.
122
+ - This must be a blocking call. wait for the `script-checker` to finish. You
123
+ don't want to surface issues after the expensive run has finished. We want to
124
+ catch all issues here.
125
+ - Two or more edits since the last `script-checker` dispatch count as one
126
+ nontrivial change on their own. Dispatch again at that point; don't wait for
127
+ it to feel big enough.
128
+ - "Issues found" loops back to `Build script/plot`, "clean" goes to `Dispatch
129
+ job-runner`.
130
+ - `advance` condition: `script-checker` has not found substantial issues, and
131
+ all issues have been handled and rechecked.
132
+
133
+ ### `Dispatch job-runner`
134
+
135
+ Heavy path continuation: launch the `job-runner` only once the
136
+ `script-checker` is clean.
137
+
138
+ - Run the `job runner` with `subagent({agent:"job-runner", task:"<script path +
139
+ output location + estimated runtime>"})`.
140
+ - Pass the runtime estimate to the `job-runner` from your smoke test in
141
+ `Light or heavy?` above rather than making it re-derive it blind.
142
+ - Register a wake instead of blocking: `subagent_wait({id, nonBlocking: true})`
143
+ after launching. Non-blocking waits allow you to continue the conversation
144
+ with the user while waiting.
145
+ - `advance` condition: `job-runner` has successfully completed the job.
146
+
147
+ ### `Summarize the answer`
148
+
149
+ - `show` the figure(s) to the user, then summarize briefly with bullets.
150
+ - Core principle: the user decides the interpretation. Continue interrogating
151
+ the user until it makes sense to you and is consistent with the dataset.
152
+ - `advance` condition: you understand the user's interpretation of the figure.
153
+
154
+ ### `Continue analysis or lock in?`
155
+
156
+ - Ask the user whether to continue with another question or lock in the
157
+ analysis; keep interrogating until they pick one. "Next question" loops back
158
+ to `Propose/ask a question`, "finish analysis" exits to the finishing skill.
159
+ - `advance` condition: the user has decided to continue the session or finish it.
160
+
161
+ ### `/skill:finishing-exploratory-data-analysis`
162
+
163
+ Figure/analysis seems meaningful, ready to write up or commit.
60
164
 
61
165
  ## Common Mistakes
62
166
 
63
167
  | Mistake | Fix |
64
168
  | --- | --- |
65
- | Treating any issue in scripts (via self-reread or `script-checker`) as a note to fold in later instead of a loop back to the script | Go to "Build script/plot" and fix it -- see the graph's "issues found" edges |
66
- | Launching a heavy scale-up while the `script-checker` is still pending or has open findings | See `checking-and-running-scripts.md`'s Common Mistakes |
169
+ | Getting the user's agreement on a confirmation stage, then acting on it without calling `advance` first | The agreement itself completes the stage -- `advance` is the first tool call once you have it, not a follow-up after next-stage work has already started |
170
+ | Treating any issue in scripts (via self-reread or `script-checker`) as a note to fold in later instead of a loop back to the script | Go to `Build script/plot` and fix it -- see the graph's "issues found" edges |
171
+ | Opening the data before agreeing on a broad scope | State the analysis's scope/question area in a sentence and confirm before `Peek at the data` |
172
+ | Picking the report/subfolder name yourself instead of asking | Ask before creating it -- one name, shared by the file and its folder |
173
+ | Treating EDA as a pipeline and auto-advancing through questions without a framing check | Confirm framing per question, not once for the whole analysis |
174
+ | Adding a follow-up figure/question under an earlier blanket go-ahead without confirming its framing | State the new framing in a sentence before scripting it |
175
+ | Building the next two questions back-to-back and presenting a "complete report" because the user said "keep going" / "I trust your judgment" | That phrase covers continued work, not skipped per-question confirmation -- stop after each one |
176
+ | Skipping the raw-row eyeball and stopping at summary stats | Read a few actual rows -- aggregates hide format/unit/encoding surprises |
177
+ | Running the full dataset (or an ad hoc exploratory probe) before a smoke test | Always sample/time on a subset first -- applies to one-off probes too, not just numbered scripts |
178
+ | Silently skipping the smoke test on a "just read cache and plot" script | Only skip for zero-computation cached-read-and-plot, and state that justification explicitly |
179
+ | Trusting a sanity check that only ran on an easy/symmetric case | Pick a case that could actually have failed, especially for matching/ordering/canonicalization logic |
180
+ | Checking that a smoke test ran fast/clean without looking at what it produced | Glance at the actual output values -- a clean run on garbage isn't a pass |
181
+ | Running a light-path script without rereading it first because it's "just quick" | Reread against the stated intent/framing before running -- speed doesn't waive the check, it just changes who does it |
182
+ | Launching a heavy scale-up "while the `script-checker` runs in the background" instead of waiting for it | Confirm the `script-checker` came back clean before launching -- don't launch and plan to fold feedback in later |
183
+ | Letting 2+ small edits pile up on a heavy-path script since the last `script-checker` dispatch because none felt big enough alone | Dispatch again once 2+ edits have landed |
184
+ | Treating a mid-work `script-checker` pass as covering the finishing skill's lock-in checkpoint | Different gate, different artifact -- a pending or completed `script-checker` pass never substitutes for it |
@@ -1,97 +0,0 @@
1
- # Checking and Running Scripts
2
-
3
- Building confidence in a script for `/skill:exploring-data`, then deciding
4
- how to run it. See `workflow.md` for where this fits in the full loop
5
- (the light/heavy fork is inlined there, each branch with its own check
6
- step and loop-back on findings).
7
-
8
- ## Sanity Checking a Script
9
-
10
- - After each nontrivial transform (groupby, join, pivot, filter, etc.), do
11
- a quick check (`.head()`, row counts, spot-check a few values) that the
12
- output matches what the transform should have done -- not just once at
13
- the end of the script.
14
- - Pick a case that could actually fail, not the first one at hand --
15
- especially for canonicalization/ordering/matching logic, where an easy
16
- case can pass while hiding a real bug.
17
-
18
- ## Smoke Testing a Script
19
-
20
- - Run on a sampled/head subset (fixed seed) and time it before running at
21
- full scale.
22
- - After it runs, glance at the actual output values, not just "it
23
- completed in time." A fast clean run on garbage output is not a
24
- passing smoke test.
25
- - Exception: a cached-read-and-plot script -- one that purely reads an
26
- already-validated cached table and plots it, with zero computation (no
27
- groupby, join, pivot, or filter). State that justification explicitly
28
- instead of skipping silently.
29
-
30
- ## Judging Heavy vs. Light Scale-up
31
-
32
- Goal: increase iteration speed without being reckless. Either extreme
33
- wastes the user's time -- blocking every quick diff on a check likely to
34
- pass, or committing to a large/expensive run that turns out wrong.
35
-
36
- - Judge from the smoke-test timing/size ratio whether the full-scale run
37
- will be heavy (long-running or memory-heavy relative to the sample --
38
- e.g. 1% took a second but full scale extrapolates to minutes).
39
- - Rough thresholds based on estimated time: light if under ~1 minute,
40
- heavy if over ~5 minutes. In between is a judgment call.
41
- - State that judgment explicitly: "sample took Xs, full run estimated ~Y,
42
- treating as heavy."
43
-
44
- ## Light Scale-up
45
-
46
- - No `script-checker` dispatch -- reread the script yourself before
47
- running: does it match the user's stated intent/framing, does each
48
- nontrivial transform do what it should.
49
- - If the reread surfaces an issue, fix it and reread again before
50
- running -- don't run first and raise the concern after.
51
- - This is a deliberately lower bar than the heavy path: a light-path
52
- mistake is cheap to catch downstream too, since the run finishes fast
53
- and you're looking at the output right away. The real second-set-of-
54
- eyes gate for the analysis still happens at
55
- `/skill:finishing-exploratory-data-analysis`'s lock-in checkpoint --
56
- this reread isn't the last line of defense, it's a cheap intermediate
57
- one.
58
-
59
- ## Heavy Scale-up
60
-
61
- - Dispatch the `script-checker` before launching:
62
- `subagent({agent:"script-checker", task:"<script path + stated intent/framing>"})`.
63
- - Do not launch until it has returned and come back clean.
64
- - If it's still running, wait for it (or say explicitly you're waiting)
65
- -- don't launch anyway planning to fold in feedback once it lands.
66
- - Launching "in parallel while the `script-checker` runs in the
67
- background" still counts as launching without it -- the expensive run
68
- finishes and gets shown to the user before a material issue could
69
- surface.
70
- - A `script-checker` pass only checks the script -- it never substitutes
71
- for the lock-in checkpoint in
72
- `/skill:finishing-exploratory-data-analysis` once a figure/question is
73
- ready to show the user.
74
- - Two or more edits since the last `script-checker` dispatch count as one
75
- nontrivial change on their own -- dispatch again at that point, don't
76
- wait for it to feel big enough.
77
-
78
- ### Dispatching the `job-runner`
79
-
80
- - Once clear to launch:
81
- `subagent({agent:"job-runner", task:"<script path + output location + estimated runtime + estimated peak memory>"})`.
82
- This frees the main session to keep iterating while it runs. Pass the
83
- runtime/memory estimate from your smoke test (see "Judging Heavy vs.
84
- Light Scale-up" above) rather than making it re-derive them blind.
85
-
86
- ## Common Mistakes
87
-
88
- | Mistake | Fix |
89
- | --- | --- |
90
- | Running the full dataset (or an ad hoc exploratory probe) before a smoke test | Always sample/time on a subset first -- applies to one-off probes too, not just numbered scripts |
91
- | Silently skipping the smoke test on a "just read cache and plot" script | Only skip for zero-computation cached-read-and-plot, and state that justification explicitly |
92
- | Trusting a sanity check that only ran on an easy/symmetric case | Pick a case that could actually have failed, especially for matching/ordering/canonicalization logic |
93
- | Checking that a smoke test ran fast/clean without looking at what it produced | Glance at the actual output values -- a clean run on garbage isn't a pass |
94
- | Running a light-path script without rereading it first because it's "just quick" | Reread against the stated intent/framing before running -- speed doesn't waive the check, it just changes who does it |
95
- | Launching a heavy scale-up "while the `script-checker` runs in the background" instead of waiting for it | Confirm the `script-checker` came back clean before launching -- don't launch and plan to fold feedback in later |
96
- | Letting 2+ small edits pile up on a heavy-path script since the last `script-checker` dispatch because none felt big enough alone | Dispatch again once 2+ edits have landed |
97
- | Treating a mid-work `script-checker` pass as covering the finishing skill's lock-in checkpoint | Different gate, different artifact -- a pending or completed `script-checker` pass never substitutes for it |
@@ -1,63 +0,0 @@
1
- # Pre-Code Checkpoints
2
-
3
- What to do for `/skill:exploring-data` before you write any
4
- code for the next question.
5
-
6
- ## Scope the analysis (once per analysis, before Peek at the data)
7
-
8
- - Before opening the data, check in one or two sentences what is broadly in
9
- scope for this analysis and get the user's confirmation. This is a "what
10
- are we even doing here", separate from and prior to the per-question framing
11
- below.
12
- - Ask the user for a report name for this analysis -- it names both the
13
- report file and its paired folder (see `directory-structure.md`).
14
- Don't pick one yourself.
15
- - Keep it broad on purpose -- treat it as provisional, not a lock. Expect
16
- it to narrow or shift as the data reveals what's actually there, well
17
- before you reach conclusions -- confirm the new framing with the user
18
- when it does, same as any other question (see below). `index.md` and
19
- the report are written together during
20
- `/skill:finishing-exploratory-data-analysis`, not during exploration.
21
-
22
- ## Peek at the data (once per new dataset, before Propose/ask a question)
23
-
24
- - Load the dataset (lazily), check schema/dtypes.
25
- - Get null counts and basic summary stats (mean/median, min/max, std).
26
- - Get a feel for the actual data: `.head()`, a strided sample, or a random
27
- sample (fixed seed).
28
- - Read a few raw rows by eye. They catch what schema/dtype checks and basic
29
- stats above miss: a "numeric" column that's really a formatted string,
30
- an ID convention, units, or what a free-text field actually holds.
31
- - Even raw rows and basic stats can miss duplicate rows.
32
- - Beyond that, think about what else is cheap to calculate and might
33
- reveal something missed above -- e.g. sentinel/placeholder values (`-1`,
34
- `0`, `"unknown"`), class imbalance, or a grain mismatch after a join.
35
-
36
- ## Propose/ask a question (once per question, every time)
37
-
38
- - Confirm the framing for *this* question/figure with the user before
39
- writing a script for it.
40
- - State the new framing in a sentence before scripting it, even if a broad
41
- go-ahead was already given for the analysis as a whole.
42
- - This applies to every question, including one you decide to add on your
43
- own after an earlier go-ahead. Approval for one question is not
44
- approval for the next one the user hasn't seen proposed yet.
45
- - **A blanket "keep going" / "I trust your judgment" is not per-question
46
- approval.** It authorizes you to keep working on framing, not to skip
47
- asking about the next question's framing. Treat it the same as
48
- silence: propose the next question's framing in one sentence, then
49
- build it.
50
- - Do not build multiple questions in one pass and hand over a "finished
51
- report" -- stop after each one and surface what it shows before
52
- moving to the next.
53
-
54
- ## Common Mistakes
55
-
56
- | Mistake | Fix |
57
- | --- | --- |
58
- | Opening the data before agreeing on a broad scope | State the analysis's scope/question area in a sentence and confirm before Peek at the data |
59
- | Picking the report/subfolder name yourself instead of asking | Ask before creating it -- one name, shared by the file and its folder |
60
- | Treating EDA as a pipeline and auto-advancing through questions without a framing check | Confirm framing per question, not once for the whole analysis |
61
- | Adding a follow-up figure/question under an earlier blanket go-ahead without confirming its framing | State the new framing in a sentence before scripting it |
62
- | Building the next two questions back-to-back and presenting a "complete report" because the user said "keep going" / "I trust your judgment" | That phrase covers continued work, not skipped per-question confirmation -- stop after each one |
63
- | Skipping the raw-row eyeball and stopping at summary stats | Read a few actual rows -- aggregates hide format/unit/encoding surprises |