pi-harness-runtime 0.2.0 → 0.3.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
@@ -1,20 +1,20 @@
1
1
  # Pi Harness Runtime
2
2
 
3
- **Codex-style `/usage` status for pi coding agent: local token tracking + manual provider mirror.**
3
+ **Autonomous AI coding harness for pi: local token tracking + provider mirror + task orchestration.**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/pi-harness-runtime?style=for-the-badge)](https://www.npmjs.com/package/pi-harness-runtime)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
7
- [![Platform](https://img.shields.io/badge/Platform-macOS%20|20|Linux-blue?style=for-the-badge)]()
7
+ [![Platform](https://img.shields.io/badge/Platform-macOS%20|Linux-blue?style=for-the-badge)]()
8
8
 
9
- ## Why Pi Harness Runtime
9
+ ## Two Flavors
10
10
 
11
- **3-Source Honest Tracking** — Codex-style `/status` for pi: local-tracked tokens + manually-mirrored provider quota + derived projections. No fake precision, no scraping, no password storage.
11
+ ### 1. `/usage` Token Tracking (v0.1+)
12
12
 
13
- **Zero Config** — Works immediately. Auto-tracks every assistant message. Run `/usage sync` once a day to mirror provider quota from `https://platform.minimax.io/console/usage`.
13
+ Codex-style usage tracking with local tokens + provider mirror.
14
14
 
15
- **Local-First Privacy** — All data stays in `~/.pi/usage-status/`. No telemetry, no remote calls, no credential storage. The provider mirror is a 5-second manual entry.
15
+ ### 2. `/harness` Autonomous Coding Loop (v0.3+)
16
16
 
17
- **Derived Insights** Auto-computes reset times from your data (no provider API needed), burn rate projections, local-vs-mirror divergence warnings.
17
+ Give the runtime a requirement. It keeps working, survives interruption, and returns with code ready for review.
18
18
 
19
19
  ## Install
20
20
 
@@ -22,186 +22,145 @@
22
22
  pi install npm:pi-harness-runtime
23
23
  ```
24
24
 
25
- Or install locally for development:
26
-
27
- ```bash
28
- git clone https://github.com/ManotLuijiu/pi-harness-runtime.git
29
- ln -s pi-harness-runtime ~/.pi/agent/extensions/pi-harness-runtime
30
- pi reload
31
- ```
32
-
33
- **Important**: Symlink the **directory** into `~/.pi/agent/extensions/`. Pi's loader scans that directory and reads `package.json`'s `pi.extensions` field for multi-file extensions. Don't symlink a single file (relative imports break) or symlink into `node_modules/` (pi doesn't auto-scan there).
34
-
35
- No API keys required. No build step (Bun runs `.ts` directly).
36
-
37
25
  Requires Pi v0.37.3+.
38
26
 
39
- ## Quick Start
27
+ ## Usage Commands
40
28
 
41
29
  ```bash
42
- /usage # show full status (model, local tracking, provider mirror)
43
- /usage sync # open form to mirror provider-side quota
44
- /usage today # focused: today's usage + 5h window
45
- /usage week # focused: this week's usage + lifetime totals
46
- /usage reset # clear provider mirror (force re-sync)
30
+ /usage # show full status (local tracking + provider mirror)
31
+ /usage sync # mirror provider quota from console.minimax.io
32
+ /usage today # today's usage + 5h window
33
+ /usage week # this week's usage + lifetime totals
34
+ /usage reset # clear provider mirror
47
35
  ```
48
36
 
49
- ## Commands
37
+ ## Harness Commands
50
38
 
51
- ### `/usage`
39
+ ```bash
40
+ /harness start <requirement> # Start a new harness job
41
+ /harness status # Show current job status
42
+ /harness tasks # List all tasks
43
+ /harness pause # Pause when quota low
44
+ /harness resume # Resume after quota reset
45
+ /harness cancel # Cancel job
46
+ ```
52
47
 
53
- Show Codex-style usage status. Renders three sections:
48
+ ## Example Session
54
49
 
55
50
  ```
56
- Codex-style usage status for pi
57
- ────────────────────────────────────────────────────────────────
58
- Model: minimax/MiniMax-M3
59
- Directory: ~/frappe-bench/apps/thai_business_suite
60
-
61
- LOCAL TRACKED (ground truth we count this)
62
- This session: $0.17 · 142k tokens · 17 requests
63
- This 5h: 384k tokens · 23 requests · $0.04
64
- This week: 1.2M tokens · 67 requests · $0.13
65
- Lifetime: 4592 requests · $81.61
66
-
67
- ② PROVIDER MIRROR (you enter from console.minimax.io)
68
- Last sync: 2 min ago [fresh]
69
- Provider: minimax
70
- 5h limit: [████████░░░░░░░░░░░░] 18% left (resets in 4h 54m)
71
- Weekly limit: [████████████████░░░░] 81% left (resets in 2d 13h)
72
-
73
- ③ LOCAL RESET TIMES (derived from your data)
74
- Local 5h reset: in 3h 12m (oldest request falls out of window)
75
- Local week reset: in 5d 7h (oldest request falls out of window)
76
- Local-vs-mirror: -12.4% ⚠️ divergence > 5%
77
- Burn rate: 11.4% / day → 100% in 2.5 d
78
- ────────────────────────────────────────────────────────────────
51
+ /harness start Build a REST API with JWT authentication
52
+
53
+ # Runtime creates task graph:
54
+ # task-001: Analyze requirements → ready
55
+ # task-002: Implement API → depends on task-001
56
+ # task-003: Write tests depends on task-002
57
+ # task-004: Code review depends on task-003
58
+
59
+ /harness status
60
+ # Job: job-123, Status: running
61
+ # Tasks: 1/4 done, 1 running
62
+
63
+ # When quota runs low:
64
+ /harness pause
65
+ # Job paused. Resume when quota resets.
66
+
67
+ # When quota resets:
68
+ /harness resume
69
+ # Job resumed. Continue working.
70
+
71
+ # When all tasks complete:
72
+ /harness status
73
+ # Job: job-123, Status: ready_for_client
74
+ # Tasks: 4/4 done ✓
79
75
  ```
80
76
 
81
- ### `/usage sync`
82
-
83
- Open a 6-prompt form to mirror provider-side quota:
77
+ ## Architecture
84
78
 
85
79
  ```
86
- 5h used % (0-100) for minimax: [18]
87
- 5h resets in (hours): [4]
88
- 5h resets in (minutes, 0-59): [56]
89
- Weekly used % (0-100): [72]
90
- Weekly resets in (days, 0-7): [2]
91
- Weekly resets in (hours, 0-23): [13]
80
+ pi-harness-runtime/
81
+ ├── index.ts # Extension entry point
82
+ ├── harness/
83
+ │ ├── job-state-machine.ts # 14-state lifecycle machine
84
+ │ ├── task-graph.ts # DAG-based task management
85
+ │ ├── master-planner.ts # Requirement task graph
86
+ │ ├── loop-runtime.ts # Core execution loop
87
+ │ ├── repair-engine.ts # Auto-fix failures
88
+ │ ├── blackboard.ts # Agent coordination
89
+ │ ├── context-window-manager.ts # Context tracking
90
+ │ ├── agent-handoff.ts # Clean agent transitions
91
+ │ ├── e2e/
92
+ │ │ ├── test-engine.ts # E2E test runner
93
+ │ │ └── playwright-runner.ts # Browser automation
94
+ │ └── project-detector/
95
+ │ └── detector.ts # Auto-detect project type
96
+ ├── packages/
97
+ │ ├── providers/adapters.ts # MiniMax, OpenAI adapters
98
+ │ ├── quota-manager/ # Quota signal collection
99
+ │ └── worktree/ # Git worktree per task
100
+ └── skills/
101
+ └── harness-runtime/
102
+ └── SKILL.md # Skill documentation
92
103
  ```
93
104
 
94
- Saves to `~/.pi/usage-status/mirror.json` and updates the next `/usage` output.
95
-
96
- ### `/usage today`
97
-
98
- Focused view: today's usage + 5h window. Quick check before starting a long session.
99
-
100
- ### `/usage week`
101
-
102
- Focused view: this week's usage + lifetime totals. Good for end-of-week review.
103
-
104
- ### `/usage reset`
105
-
106
- Clear the provider mirror. Local usage log is preserved. Asks for confirmation.
107
-
108
- ## How It Works
109
-
110
- ### Data Sources (3-source model)
111
-
112
- 1. **Local tracked** — every assistant message is logged to `~/.pi/usage-status/usage.jsonl`
113
- - Auto-tracked via `message_end` event
114
- - Contains: timestamp, model, input/output/cache tokens, cost
115
- - Real-time, exact, but only counts THIS pi session
116
-
117
- 2. **Provider mirror** — manually entered from `https://platform.minimax.io/console/usage`
118
- - Stored at `~/.pi/usage-status/mirror.json`
119
- - Synced via `/usage sync` form
120
- - Ground truth for TOTAL quota (across all clients)
105
+ ## Job State Machine
121
106
 
122
- 3. **Derived** — burn rate, reset times, divergence
123
- - Local reset time = oldest request in window + window duration
124
- - Burn rate = mirror weekly % / elapsed days
125
- - Divergence warning if local tracking differs from mirror by >5%
126
-
127
- ### Files Written
128
-
129
- | Path | Contents |
130
- |---|---|
131
- | `~/.pi/usage-status/usage.jsonl` | Append-only usage log |
132
- | `~/.pi/usage-status/mirror.json` | Manual provider mirror |
133
- | (none — pure functions) | CLI helpers in `cli.ts` |
134
-
135
- Override location with `PI_USAGE_DIR` env var (useful for testing).
136
-
137
- ### What's NOT Stored
138
-
139
- - Your password (we don't ask for it, ever)
140
- - Provider session tokens (we don't read browser cookies)
141
- - Telemetry or remote calls (all local)
142
- - Auto-send quota (you always confirm any sync)
143
-
144
- ## Why Manual Mirror?
107
+ ```
108
+ created planning queued running testing reviewing
109
+ ↓ ↓ ↓ ↓ ↓
110
+ cancelled blocked waiting_human repairing ready_for_client
111
+ ↓ ↓
112
+ paused_quota archived
113
+ ```
145
114
 
146
- Most AI providers (MiniMax, Anthropic, OpenAI) don't expose rate limit headers publicly. Two alternatives exist:
115
+ ## Key Features
147
116
 
148
- | Approach | Pros | Cons |
149
- |---|---|---|
150
- | **Manual mirror** (this) | Zero security risk, 5 sec/day | 1 manual entry |
151
- | **Browser cookie extraction** (pi-web-access does for Gemini Web) | Fully automatic | ~400 LOC platform-specific code, fragile to auth changes |
152
- | **HTML scraping** (assumes cookies work) | Once cookies extracted, parsing is easy | Breaks if page redesigns |
117
+ - **Resumable**: Every state change is checkpointed to disk
118
+ - **Quota-aware**: Detects quota exhaustion, pauses, resumes after reset
119
+ - **Provider-agnostic**: MiniMax, OpenAI, Claude adapters
120
+ - **Task DAG**: Dependencies tracked, topological execution
121
+ - **Auto-repair**: Failure classification + retry with exponential backoff
122
+ - **E2E testing**: Scenario-based Playwright integration
123
+ - **Project detection**: Auto-detects Frappe, Next.js, React, Django, Laravel
153
124
 
154
- We chose the manual approach because:
125
+ ## Data Directory
155
126
 
156
- 1. MiniMax's UI shows reset times like "Resets in 4 hr 56 min" — manual entry is genuinely 5 seconds
157
- 2. Cookie extraction has ongoing maintenance burden
158
- 3. Password storage is a security anti-pattern
127
+ All data stored locally in `~/.pi/`:
159
128
 
160
- If you find yourself needing to sync more than 3x/day, we may add browser cookie extraction as a future enhancement.
129
+ ```
130
+ ~/.pi/
131
+ ├── usage-status/ # /usage data
132
+ │ ├── usage.jsonl
133
+ │ └── mirror.json
134
+ └── harness/ # /harness data
135
+ └── jobs/
136
+ └── <job-id>/
137
+ ├── checkpoint.json
138
+ ├── events.jsonl
139
+ ├── task-graph.json
140
+ ├── blackboard/
141
+ └── repair-tasks.jsonl
142
+ ```
161
143
 
162
144
  ## Safety Properties
163
145
 
164
- - ✅ **No auto-tracking of other clients** — local data is just this pi session
165
- - ✅ **No scraping** provider mirror is manual
166
- - ✅ **No fabrication** divergence warning if local and mirror disagree
167
- - ✅ **Idempotent** running `/usage` repeatedly has no side effects
168
- - ✅ **Privacy-respecting** all data stays on local disk
169
- - ✅ **No credentials stored** — passwords, tokens, cookies: none
146
+ - ✅ **Local-first**: All data stays on disk
147
+ - ✅ **No credentials stored**: No passwords, tokens, or cookies
148
+ - ✅ **Human-on-the-loop**: Clear intervention points
149
+ - ✅ **Checkpointed**: Resume from any state
150
+ - ✅ **Idempotent**: Safe to run multiple times
170
151
 
171
- ## Architecture
152
+ ## Testing
172
153
 
154
+ ```bash
155
+ bun test # 131+ tests passing
173
156
  ```
174
- pi-harness-runtime/
175
- ├── index.ts # entry — exports default (pi: ExtensionAPI)
176
- ├── tracker.ts # UsageTracker — JSONL writer/reader
177
- ├── mirror.ts # MirrorStore — JSON read/write + freshness check
178
- ├── windows.ts # WindowAggregator — pure aggregation functions
179
- ├── renderer.ts # StatusRenderer — Codex-style progress bars
180
- ├── sync-form.ts # /usage sync form handling
181
- ├── cli.ts # shared pure helpers (paths, formatting, IO)
182
- ├── skills/
183
- │ └── harness-runtime/
184
- │ └── SKILL.md # bundled skill (auto-loaded)
185
- ├── test/ # node --test
186
- │ ├── cli.test.mjs
187
- │ ├── tracker.test.mjs
188
- │ ├── mirror.test.mjs
189
- │ ├── windows.test.mjs
190
- │ ├── renderer.test.mjs
191
- │ └── sync-form.test.mjs
192
- ├── package.json # "pi" field declares extensions + skills
193
- ├── README.md
194
- ├── CHANGELOG.md
195
- ├── LICENSE # MIT
196
- └── .gitignore
197
- ```
198
-
199
- ## Related
200
-
201
- - [pi-web-access](https://github.com/nicobailon/pi-web-access) — web search + URL fetching for pi (inspiration for this package's structure)
202
- - [context-mode](https://github.com/MiniMax-AI/context-mode) — context-window tracking (complementary)
203
- - [pi-coding-agent](https://github.com/earendil-works/pi-coding-agent) — the underlying pi agent
204
157
 
205
158
  ## License
206
159
 
207
160
  MIT © 2026 MooCoding
161
+
162
+ ## Related
163
+
164
+ - [pi-coding-agent](https://github.com/earendil-works/pi-coding-agent) — Underlying pi agent
165
+ - [context-mode](https://github.com/MiniMax-AI/context-mode) — Context window tracking
166
+ - [pi-web-access](https://github.com/nicobailon/pi-web-access) — Web search for pi
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Agent Handoff Protocol — RFC-0012
3
+ *
4
+ * Clean handoff between agents with context transfer.
5
+ * Ensures continuity when switching agents mid-task.
6
+ */
7
+
8
+ import type {
9
+ HandoffContext,
10
+ HandoffEvent,
11
+ } from "../packages/types/src/runtime-types.ts";
12
+ import { writeJson, readJson } from "../cli.ts";
13
+ // @ts-expect-error - Bun has built-in Node.js types
14
+ import { join } from "node:path";
15
+
16
+ export interface HandoffData {
17
+ fromAgent: string;
18
+ toAgent: string;
19
+ taskId: string;
20
+ taskSummary: string;
21
+ contextFiles: string[];
22
+ recentHistory: string[];
23
+ currentState: Record<string, unknown>;
24
+ }
25
+
26
+ export class AgentHandoffProtocol {
27
+ private readonly rootDir: string;
28
+
29
+ constructor(rootDir: string) {
30
+ this.rootDir = rootDir;
31
+ }
32
+
33
+ /**
34
+ * Create a handoff context for switching agents
35
+ */
36
+ createHandoff(
37
+ jobId: string,
38
+ taskId: string,
39
+ fromAgent: string,
40
+ toAgent: string,
41
+ currentState?: Record<string, unknown>,
42
+ ): HandoffContext {
43
+ const events = this.loadHandoffHistory(jobId, taskId);
44
+
45
+ return {
46
+ jobId,
47
+ taskId,
48
+ fromAgent,
49
+ toAgent,
50
+ sharedFiles: [],
51
+ taskHistory: events,
52
+ summary: this.generateSummary(taskId, currentState),
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Record a handoff event
58
+ */
59
+ recordHandoff(context: HandoffContext, result?: string): void {
60
+ const path = join(
61
+ this.rootDir,
62
+ "jobs",
63
+ context.jobId,
64
+ "handoffs",
65
+ `${context.taskId}.json`,
66
+ );
67
+ const event: HandoffEvent = {
68
+ ts: new Date().toISOString(),
69
+ agentId: context.toAgent,
70
+ action: "handoff_received",
71
+ result,
72
+ };
73
+ context.taskHistory.push(event);
74
+ writeJson(path, context);
75
+ }
76
+
77
+ /**
78
+ * Generate handoff prompt for the receiving agent
79
+ */
80
+ generateHandoffPrompt(context: HandoffContext): string {
81
+ const lines = [
82
+ `## Agent Handoff`,
83
+ ``,
84
+ `**From Agent:** ${context.fromAgent}`,
85
+ `**To Agent:** ${context.toAgent}`,
86
+ `**Task:** ${context.taskId}`,
87
+ ``,
88
+ `### Task History`,
89
+ ];
90
+
91
+ for (const event of context.taskHistory) {
92
+ lines.push(`- [${event.ts}] ${event.agentId}: ${event.action}`);
93
+ if (event.result) {
94
+ lines.push(` Result: ${event.result}`);
95
+ }
96
+ }
97
+
98
+ lines.push(``);
99
+ lines.push(`### Summary`);
100
+ lines.push(context.summary);
101
+
102
+ if (context.sharedFiles.length > 0) {
103
+ lines.push(``);
104
+ lines.push(`### Shared Files`);
105
+ for (const file of context.sharedFiles) {
106
+ lines.push(`- ${file}`);
107
+ }
108
+ }
109
+
110
+ return lines.join("\n");
111
+ }
112
+
113
+ /**
114
+ * Validate handoff readiness
115
+ */
116
+ validateHandoff(context: HandoffContext): {
117
+ valid: boolean;
118
+ issues: string[];
119
+ } {
120
+ const issues: string[] = [];
121
+
122
+ if (!context.summary) {
123
+ issues.push("Task summary is empty");
124
+ }
125
+
126
+ if (context.taskHistory.length === 0) {
127
+ issues.push("No task history recorded");
128
+ }
129
+
130
+ // Check for recent handoffs
131
+ const recentHandoffs = context.taskHistory.filter((h) => {
132
+ const age = Date.now() - Date.parse(h.ts);
133
+ return age < 5 * 60 * 1000; // 5 minutes
134
+ });
135
+
136
+ if (recentHandoffs.length > 3) {
137
+ issues.push(
138
+ `Too many recent handoffs (${recentHandoffs.length}). Possible ping-pong.`,
139
+ );
140
+ }
141
+
142
+ return { valid: issues.length === 0, issues };
143
+ }
144
+
145
+ /**
146
+ * Load handoff history for a task
147
+ */
148
+ private loadHandoffHistory(jobId: string, taskId: string): HandoffEvent[] {
149
+ const path = join(
150
+ this.rootDir,
151
+ "jobs",
152
+ jobId,
153
+ "handoffs",
154
+ `${taskId}.json`,
155
+ );
156
+ const data = readJson(path) as HandoffContext | null;
157
+ return data?.taskHistory ?? [];
158
+ }
159
+
160
+ /**
161
+ * Generate a summary of the task state
162
+ */
163
+ private generateSummary(
164
+ taskId: string,
165
+ currentState?: Record<string, unknown>,
166
+ ): string {
167
+ if (!currentState) {
168
+ return `Task ${taskId} requires continuation. Check task files for current state.`;
169
+ }
170
+
171
+ const lines = [`Task ${taskId} is in progress.`];
172
+
173
+ if (currentState.filesModified) {
174
+ lines.push(
175
+ `Files modified: ${(currentState.filesModified as string[]).join(", ")}`,
176
+ );
177
+ }
178
+
179
+ if (currentState.lastAction) {
180
+ lines.push(`Last action: ${currentState.lastAction}`);
181
+ }
182
+
183
+ if (currentState.blockers) {
184
+ lines.push(`Blockers: ${currentState.blockers}`);
185
+ }
186
+
187
+ return lines.join("\n");
188
+ }
189
+ }