pi-harness-runtime 1.1.44 → 1.1.45
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/harness/langchain/agents.ts +15 -3
- package/harness/langchain/daemon.ts +126 -1
- package/harness/langchain/file-tools.ts +178 -0
- package/harness/langchain/graph.ts +10 -5
- package/harness/langchain/status-line.test.ts +92 -0
- package/harness/langchain/status-line.ts +255 -0
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { createAgent, tool } from "langchain";
|
|
21
21
|
import { z } from "zod";
|
|
22
|
+
import { fileTools } from "./file-tools.js";
|
|
22
23
|
import {
|
|
23
24
|
createCoderModel,
|
|
24
25
|
createPlannerModel,
|
|
@@ -103,8 +104,19 @@ The loop will route automatically.`;
|
|
|
103
104
|
const CODER_PROMPT = `You are the Coding Agent (MiniMax) in an autonomous daemon loop.
|
|
104
105
|
|
|
105
106
|
You receive a plan (and possibly review comments from a previous iteration) and
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
edit the actual repository files using the available tools. Address every review
|
|
108
|
+
comment explicitly.
|
|
109
|
+
|
|
110
|
+
Tools available:
|
|
111
|
+
- read_file(path): Read a file's contents before editing
|
|
112
|
+
- write_file(path, content, append?): Write or append to a file
|
|
113
|
+
- list_directory(path): List files in a directory
|
|
114
|
+
|
|
115
|
+
Workflow:
|
|
116
|
+
1. Use read_file to inspect existing files
|
|
117
|
+
2. Use write_file to create or update files with real code
|
|
118
|
+
3. Use list_directory to explore the project structure
|
|
119
|
+
4. Write real files, not code blocks — the loop will review the actual edits
|
|
108
120
|
|
|
109
121
|
IMPORTANT:
|
|
110
122
|
- Do NOT output "Next Steps", "Ready for review", or any waiting-for-human signal
|
|
@@ -147,7 +159,7 @@ export function createPlannerAgent(opts: ModelOptions = {}) {
|
|
|
147
159
|
export function createCoderAgent(opts: ModelOptions = {}) {
|
|
148
160
|
return createAgent({
|
|
149
161
|
model: createCoderModel(opts),
|
|
150
|
-
tools:
|
|
162
|
+
tools: fileTools,
|
|
151
163
|
systemPrompt: CODER_PROMPT,
|
|
152
164
|
});
|
|
153
165
|
}
|
|
@@ -49,6 +49,7 @@ import { NotificationCenter } from "../../packages/notification/dist/notificatio
|
|
|
49
49
|
|
|
50
50
|
import { invokeWithSurgeRetry, type SurgePolicy } from "./surge.js";
|
|
51
51
|
import type { LoopWidget } from "./widget.js";
|
|
52
|
+
import { StatusLineManager, isPiLensAvailable } from "./status-line.js";
|
|
52
53
|
import { createLoopCheckpointer } from "./checkpointer.js";
|
|
53
54
|
|
|
54
55
|
import {
|
|
@@ -518,6 +519,117 @@ class BdTasksWatcher {
|
|
|
518
519
|
}
|
|
519
520
|
}
|
|
520
521
|
|
|
522
|
+
// ─── Cron Source ──────────────────────────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Watches a cron-tasks directory and fires tasks on a configurable schedule.
|
|
526
|
+
*
|
|
527
|
+
* Storage layout:
|
|
528
|
+
* <workspace>/.cron-tasks/
|
|
529
|
+
* ├── every-5m/
|
|
530
|
+
* │ ├── task-1.json {"request": "...", "enabled": true}
|
|
531
|
+
* │ └── task-2.json
|
|
532
|
+
* ├── hourly/
|
|
533
|
+
* │ └── task-3.json
|
|
534
|
+
* └── daily/
|
|
535
|
+
* └── task-4.json
|
|
536
|
+
*
|
|
537
|
+
* Schedule buckets: "every-5m", "every-10m", "every-30m", "hourly", "daily", "weekly"
|
|
538
|
+
* Drop a JSON file into the right bucket to schedule a task.
|
|
539
|
+
* Set "enabled": false to pause without deleting.
|
|
540
|
+
*/
|
|
541
|
+
class CronWatcher {
|
|
542
|
+
private readonly workspace: string;
|
|
543
|
+
private readonly scheduleMs: Record<string, number>;
|
|
544
|
+
private readonly seen: Map<string, Set<string>> = new Map();
|
|
545
|
+
private readonly timers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
546
|
+
private running = false;
|
|
547
|
+
|
|
548
|
+
constructor(workspace: string) {
|
|
549
|
+
this.workspace = workspace;
|
|
550
|
+
this.scheduleMs = {
|
|
551
|
+
"every-5m": 5 * 60_000,
|
|
552
|
+
"every-10m": 10 * 60_000,
|
|
553
|
+
"every-30m": 30 * 60_000,
|
|
554
|
+
hourly: 3_600_000,
|
|
555
|
+
daily: 86_400_000,
|
|
556
|
+
weekly: 7 * 86_400_000,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
start(onTask: (task: TriggeredTask) => void): void {
|
|
561
|
+
this.running = true;
|
|
562
|
+
const cronDir = `${this.workspace}/.cron-tasks`;
|
|
563
|
+
import("node:fs").then(({ mkdirSync, existsSync }) => {
|
|
564
|
+
if (!existsSync(cronDir)) mkdirSync(cronDir, { recursive: true });
|
|
565
|
+
});
|
|
566
|
+
// Fire once at start
|
|
567
|
+
this._tickAll(onTask);
|
|
568
|
+
// Schedule each bucket
|
|
569
|
+
for (const [schedule, ms] of Object.entries(this.scheduleMs)) {
|
|
570
|
+
this._scheduleNext(schedule, ms, onTask);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
stop(): void {
|
|
575
|
+
this.running = false;
|
|
576
|
+
for (const t of this.timers.values()) clearTimeout(t);
|
|
577
|
+
this.timers.clear();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private _scheduleNext(
|
|
581
|
+
schedule: string,
|
|
582
|
+
ms: number,
|
|
583
|
+
onTask: (task: TriggeredTask) => void,
|
|
584
|
+
): void {
|
|
585
|
+
const timer = setTimeout(async () => {
|
|
586
|
+
if (!this.running) return;
|
|
587
|
+
await this._tickSchedule(schedule, onTask);
|
|
588
|
+
this._scheduleNext(schedule, ms, onTask);
|
|
589
|
+
}, ms);
|
|
590
|
+
this.timers.set(schedule, timer);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private async _tickAll(onTask: (task: TriggeredTask) => void): Promise<void> {
|
|
594
|
+
for (const schedule of Object.keys(this.scheduleMs)) {
|
|
595
|
+
await this._tickSchedule(schedule, onTask);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private async _tickSchedule(
|
|
600
|
+
schedule: string,
|
|
601
|
+
onTask: (task: TriggeredTask) => void,
|
|
602
|
+
): Promise<void> {
|
|
603
|
+
if (!this.running) return;
|
|
604
|
+
const { readdirSync, readFileSync, existsSync } = await import("node:fs");
|
|
605
|
+
const cronDir = `${this.workspace}/.cron-tasks/${schedule}`;
|
|
606
|
+
if (!existsSync(cronDir)) return;
|
|
607
|
+
|
|
608
|
+
let seen = this.seen.get(schedule);
|
|
609
|
+
if (!seen) { seen = new Set(); this.seen.set(schedule, seen); }
|
|
610
|
+
|
|
611
|
+
let files: string[];
|
|
612
|
+
try { files = readdirSync(cronDir).filter((f) => f.endsWith(".json")); }
|
|
613
|
+
catch { return; }
|
|
614
|
+
|
|
615
|
+
for (const file of files) {
|
|
616
|
+
if (seen.has(file)) continue;
|
|
617
|
+
seen.add(file);
|
|
618
|
+
let taskData: { request?: string; enabled?: boolean };
|
|
619
|
+
try { taskData = JSON.parse(readFileSync(`${cronDir}/${file}`, "utf8")); }
|
|
620
|
+
catch { continue; }
|
|
621
|
+
if (taskData.enabled === false) continue;
|
|
622
|
+
if (!taskData.request) continue;
|
|
623
|
+
onTask({
|
|
624
|
+
taskId: `cron:${schedule}:${file.replace(".json", "")}`,
|
|
625
|
+
request: taskData.request,
|
|
626
|
+
source: "cron",
|
|
627
|
+
triggeredAt: new Date().toISOString(),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
521
633
|
// ─── Tunnel Health Monitor ───────────────────────────────────────────────────
|
|
522
634
|
|
|
523
635
|
/**
|
|
@@ -674,7 +786,9 @@ export class LoopDaemon {
|
|
|
674
786
|
private readonly inboxWatcher: InboxWatcher;
|
|
675
787
|
private readonly busWatcher: BusWatcher;
|
|
676
788
|
private readonly bdWatcher: BdTasksWatcher;
|
|
789
|
+
private readonly cronWatcher: CronWatcher | null;
|
|
677
790
|
private readonly tunnelMonitor: TunnelMonitor | null;
|
|
791
|
+
private readonly statusLine: StatusLineManager | null;
|
|
678
792
|
private readonly widget: LoopWidget | null;
|
|
679
793
|
|
|
680
794
|
/** Currently running tasks (taskId → RunningTask) */
|
|
@@ -730,6 +844,12 @@ export class LoopDaemon {
|
|
|
730
844
|
this.widget = this.config.widget ?? null;
|
|
731
845
|
this.busWatcher = new BusWatcher(this.bus, this.config.pollMs);
|
|
732
846
|
this.bdWatcher = new BdTasksWatcher(this.config.pollMs);
|
|
847
|
+
this.cronWatcher = this.config.sources.includes("cron")
|
|
848
|
+
? new CronWatcher(this.bus.getWorkspace())
|
|
849
|
+
: null;
|
|
850
|
+
this.statusLine = isPiLensAvailable()
|
|
851
|
+
? null
|
|
852
|
+
: new StatusLineManager(this.bus.getWorkspace());
|
|
733
853
|
|
|
734
854
|
// Tunnel monitor — only active when a tunnel script is configured
|
|
735
855
|
if (this.config.tunnelCommand) {
|
|
@@ -772,6 +892,7 @@ export class LoopDaemon {
|
|
|
772
892
|
}
|
|
773
893
|
|
|
774
894
|
this.tunnelMonitor?.start();
|
|
895
|
+
this.statusLine?.start();
|
|
775
896
|
|
|
776
897
|
console.log("[daemon] Started.");
|
|
777
898
|
}
|
|
@@ -784,6 +905,8 @@ export class LoopDaemon {
|
|
|
784
905
|
this.inboxWatcher.stop();
|
|
785
906
|
this.busWatcher.stop();
|
|
786
907
|
this.bdWatcher.stop();
|
|
908
|
+
this.cronWatcher?.stop();
|
|
909
|
+
this.statusLine?.stop();
|
|
787
910
|
this.tunnelMonitor?.stop();
|
|
788
911
|
|
|
789
912
|
// Release all held leases
|
|
@@ -888,7 +1011,7 @@ export class LoopDaemon {
|
|
|
888
1011
|
maxIterations: this.config.maxIterations,
|
|
889
1012
|
blackboardDir: this.bus.getWorkspace(),
|
|
890
1013
|
})
|
|
891
|
-
|
|
1014
|
+
: await buildRealLoopDeps({
|
|
892
1015
|
maxIterations: this.config.maxIterations,
|
|
893
1016
|
blackboardDir: this.bus.getWorkspace(),
|
|
894
1017
|
}));
|
|
@@ -977,6 +1100,8 @@ export class LoopDaemon {
|
|
|
977
1100
|
: "max_iterations",
|
|
978
1101
|
);
|
|
979
1102
|
}
|
|
1103
|
+
const loopDone = verdict === "approved" ? "✅ finished" : verdict === "blocked" ? "⛔ blocked" : "🔄 max_iter";
|
|
1104
|
+
this.statusLine?.updateLoopStatus(loopDone);
|
|
980
1105
|
|
|
981
1106
|
// ── Gate check after blocked verdict ────────────────────────────────
|
|
982
1107
|
if (
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File system tools for the coder agent — read and write files on disk.
|
|
3
|
+
*
|
|
4
|
+
* These tools let the MiniMax coder edit the actual repository instead of
|
|
5
|
+
* just outputting code blocks. Tools are plain Node.js fs wrappers; the
|
|
6
|
+
* agent receives structured JSON results it can act on.
|
|
7
|
+
*
|
|
8
|
+
* Wiki: wiki/multi-agent-langchain.md §file-tools
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { tool } from "langchain";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, statSync } from "node:fs";
|
|
14
|
+
import { join, isAbsolute, resolve } from "node:path";
|
|
15
|
+
|
|
16
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/** Resolve a path relative to repoRoot or as absolute. */
|
|
19
|
+
function resolvePath(path: string, repoRoot: string): string {
|
|
20
|
+
if (isAbsolute(path)) return path;
|
|
21
|
+
return resolve(repoRoot, path);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Tools ──────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read the contents of a file.
|
|
28
|
+
*
|
|
29
|
+
* Use this before editing to see the current code.
|
|
30
|
+
* Returns the full file contents as a string.
|
|
31
|
+
*/
|
|
32
|
+
export const readFile = tool(
|
|
33
|
+
async ({
|
|
34
|
+
path,
|
|
35
|
+
encoding = "utf-8",
|
|
36
|
+
}: {
|
|
37
|
+
/** Absolute path or path relative to the repository root */
|
|
38
|
+
path: string;
|
|
39
|
+
/** File encoding (default: utf-8) */
|
|
40
|
+
encoding?: string;
|
|
41
|
+
}) => {
|
|
42
|
+
try {
|
|
43
|
+
const resolved = resolvePath(path, process.cwd());
|
|
44
|
+
if (!existsSync(resolved)) {
|
|
45
|
+
return JSON.stringify({ error: `File not found: ${resolved}` });
|
|
46
|
+
}
|
|
47
|
+
const stats = statSync(resolved);
|
|
48
|
+
if (stats.isDirectory()) {
|
|
49
|
+
return JSON.stringify({ error: `Path is a directory, not a file: ${resolved}` });
|
|
50
|
+
}
|
|
51
|
+
const content = readFileSync(resolved, encoding as BufferEncoding);
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
path: resolved,
|
|
54
|
+
size: stats.size,
|
|
55
|
+
lines: content.split("\n").length,
|
|
56
|
+
content,
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return JSON.stringify({
|
|
60
|
+
error: `Failed to read file: ${path}`,
|
|
61
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "read_file",
|
|
67
|
+
description:
|
|
68
|
+
"Read the full contents of a file from disk. " +
|
|
69
|
+
"Returns the file content as a string. " +
|
|
70
|
+
"Use this before editing a file so you know its current contents. " +
|
|
71
|
+
"Returns JSON with path, size, lines, and content fields.",
|
|
72
|
+
schema: z.object({
|
|
73
|
+
path: z.string().describe("Absolute path or path relative to the repository root"),
|
|
74
|
+
encoding: z
|
|
75
|
+
.enum(["utf-8", "utf-16", "ascii", "base64"])
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("File encoding (default: utf-8)"),
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Write or overwrite a file on disk.
|
|
84
|
+
*
|
|
85
|
+
* Creates parent directories if they don't exist.
|
|
86
|
+
* The caller (an agent) is responsible for ensuring the content is correct.
|
|
87
|
+
*/
|
|
88
|
+
export const writeFile = tool(
|
|
89
|
+
async ({
|
|
90
|
+
path,
|
|
91
|
+
content,
|
|
92
|
+
append = false,
|
|
93
|
+
}: {
|
|
94
|
+
/** Absolute path or path relative to the repository root */
|
|
95
|
+
path: string;
|
|
96
|
+
/** The content to write. Use \\n for newlines. */
|
|
97
|
+
content: string;
|
|
98
|
+
/** If true, append to the file instead of overwriting (default: false) */
|
|
99
|
+
append?: boolean;
|
|
100
|
+
}) => {
|
|
101
|
+
try {
|
|
102
|
+
const resolved = resolvePath(path, process.cwd());
|
|
103
|
+
const mode = append ? "a" : "w";
|
|
104
|
+
// Normalize content: convert literal \n sequences to actual newlines
|
|
105
|
+
const normalized = content.replace(/\\n/g, "\n");
|
|
106
|
+
writeFileSync(resolved, normalized, { encoding: "utf-8", flag: mode });
|
|
107
|
+
const stats = statSync(resolved);
|
|
108
|
+
return JSON.stringify({
|
|
109
|
+
ok: true,
|
|
110
|
+
path: resolved,
|
|
111
|
+
size: stats.size,
|
|
112
|
+
action: append ? "appended" : "written",
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return JSON.stringify({
|
|
116
|
+
error: `Failed to write file: ${path}`,
|
|
117
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "write_file",
|
|
123
|
+
description:
|
|
124
|
+
"Write or overwrite a file on disk. Creates parent directories if needed. " +
|
|
125
|
+
"Use literal \\n in the content string for newlines. " +
|
|
126
|
+
"Set append=true to add to the end of an existing file. " +
|
|
127
|
+
"Returns JSON with ok, path, size, and action fields.",
|
|
128
|
+
schema: z.object({
|
|
129
|
+
path: z.string().describe("Absolute path or path relative to the repository root"),
|
|
130
|
+
content: z.string().describe("File content. Use \\n for newlines."),
|
|
131
|
+
append: z
|
|
132
|
+
.boolean()
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Append to existing file instead of overwriting (default: false)"),
|
|
135
|
+
}),
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* List files in a directory (non-recursive).
|
|
141
|
+
*
|
|
142
|
+
* Useful for discovering the project structure.
|
|
143
|
+
*/
|
|
144
|
+
export const listDir = tool(
|
|
145
|
+
async ({ path }: { path: string }) => {
|
|
146
|
+
try {
|
|
147
|
+
const { readdirSync } = await import("node:fs");
|
|
148
|
+
const resolved = resolvePath(path, process.cwd());
|
|
149
|
+
if (!existsSync(resolved)) {
|
|
150
|
+
return JSON.stringify({ error: `Directory not found: ${resolved}` });
|
|
151
|
+
}
|
|
152
|
+
const entries = readdirSync(resolved, { withFileTypes: true });
|
|
153
|
+
const files = entries.map((e) => ({
|
|
154
|
+
name: e.name,
|
|
155
|
+
type: e.isDirectory() ? "dir" : "file",
|
|
156
|
+
path: join(resolved, e.name),
|
|
157
|
+
}));
|
|
158
|
+
return JSON.stringify({ path: resolved, entries: files });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return JSON.stringify({
|
|
161
|
+
error: `Failed to list directory: ${path}`,
|
|
162
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "list_directory",
|
|
168
|
+
description:
|
|
169
|
+
"List the contents of a directory (files and subdirectories, non-recursive). " +
|
|
170
|
+
"Returns JSON with path and entries array (each entry has name, type, path).",
|
|
171
|
+
schema: z.object({
|
|
172
|
+
path: z.string().describe("Absolute path or path relative to the repository root"),
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
/** All file system tools, ready to pass to `createCoderAgent`. */
|
|
178
|
+
export const fileTools = [readFile, writeFile, listDir];
|
|
@@ -232,7 +232,6 @@ export async function buildRealLoopDeps(
|
|
|
232
232
|
const _autonomous = false;
|
|
233
233
|
const _directive = "";
|
|
234
234
|
|
|
235
|
-
|
|
236
235
|
/** Inject scoreboard markdown into a user message. */
|
|
237
236
|
const withScoreboard = (msg: string) => {
|
|
238
237
|
const scoreboard = blackboard.toMarkdown();
|
|
@@ -278,13 +277,15 @@ export async function buildRealLoopDeps(
|
|
|
278
277
|
}
|
|
279
278
|
};
|
|
280
279
|
|
|
281
|
-
|
|
280
|
+
return {
|
|
282
281
|
maxIterations: options.maxIterations ?? 3,
|
|
283
282
|
onStep,
|
|
284
283
|
plan: async (request) =>
|
|
285
284
|
lastMessage(
|
|
286
285
|
await planner.invoke({
|
|
287
|
-
messages: [
|
|
286
|
+
messages: [
|
|
287
|
+
{ role: "user", content: _directive + withScoreboard(request) },
|
|
288
|
+
],
|
|
288
289
|
}),
|
|
289
290
|
),
|
|
290
291
|
write: async (plan, review) => {
|
|
@@ -295,7 +296,9 @@ export async function buildRealLoopDeps(
|
|
|
295
296
|
: `## Plan\n${plan}`;
|
|
296
297
|
return lastMessage(
|
|
297
298
|
await coder.invoke({
|
|
298
|
-
messages: [
|
|
299
|
+
messages: [
|
|
300
|
+
{ role: "user", content: _directive + withScoreboard(userMsg) },
|
|
301
|
+
],
|
|
299
302
|
}),
|
|
300
303
|
);
|
|
301
304
|
},
|
|
@@ -304,7 +307,9 @@ export async function buildRealLoopDeps(
|
|
|
304
307
|
messages: [
|
|
305
308
|
{
|
|
306
309
|
role: "user",
|
|
307
|
-
content:
|
|
310
|
+
content:
|
|
311
|
+
_directive +
|
|
312
|
+
withScoreboard(`## Plan\n${plan}\n\n## Code to review\n${code}`),
|
|
308
313
|
},
|
|
309
314
|
],
|
|
310
315
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusLineManager tests — T16
|
|
3
|
+
*
|
|
4
|
+
* Tests the pi-lens detection, quota formatting, and status file writing
|
|
5
|
+
* when pi-lens is not available.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
9
|
+
import assert from "node:assert";
|
|
10
|
+
import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { StatusLineManager, isPiLensAvailable, formatQuotaLine } from "./status-line.js";
|
|
13
|
+
|
|
14
|
+
describe("isPiLensAvailable", () => {
|
|
15
|
+
it("returns false when no PI_LENS env vars are set", () => {
|
|
16
|
+
assert.ok(!isPiLensAvailable());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("formatQuotaLine", () => {
|
|
21
|
+
it("returns 'quota unknown' when no data", () => {
|
|
22
|
+
assert.ok(formatQuotaLine({}).startsWith("MiniMax: quota unknown"));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("shows 5h usage when available", () => {
|
|
26
|
+
const line = formatQuotaLine({ fiveHourPercent: 89 });
|
|
27
|
+
assert.ok(line.includes("5h: 89% left"), line);
|
|
28
|
+
assert.ok(line.includes("MiniMax:"), line);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("shows both 5h and weekly when both available", () => {
|
|
32
|
+
const line = formatQuotaLine({ fiveHourPercent: 89, weeklyPercent: 75 });
|
|
33
|
+
assert.ok(line.includes("5h: 89% left"), line);
|
|
34
|
+
assert.ok(line.includes("week: 75% left"), line);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("shows 0% left when at 100%", () => {
|
|
38
|
+
const line = formatQuotaLine({ fiveHourPercent: 100 });
|
|
39
|
+
assert.ok(line.includes("0% left"), line);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("shows ? for invalid negative percentage", () => {
|
|
43
|
+
// fmtPercent returns ? for negative values
|
|
44
|
+
const line = formatQuotaLine({ fiveHourPercent: -1 });
|
|
45
|
+
assert.ok(line.includes("5h: ?"), line);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("shows reset countdown when resetAt is provided", () => {
|
|
49
|
+
const future = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(); // 2h from now
|
|
50
|
+
const line = formatQuotaLine({ fiveHourPercent: 89, fiveHourResetAt: future });
|
|
51
|
+
assert.ok(line.includes("resets in"), line);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("StatusLineManager", () => {
|
|
56
|
+
const tmpDir = join(process.env.TMPDIR ?? "/tmp", `harness-status-test-${Date.now()}`);
|
|
57
|
+
let manager: StatusLineManager;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
61
|
+
manager = new StatusLineManager(tmpDir);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
manager.stop();
|
|
66
|
+
try { rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("getLine returns combined loop + quota status", () => {
|
|
70
|
+
manager.updateLoopStatus("🧭 planning");
|
|
71
|
+
const line = manager.getLine();
|
|
72
|
+
assert.ok(line.includes("planning"), line);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("getLine returns quota-only when no loop status", () => {
|
|
76
|
+
const line = manager.getLine();
|
|
77
|
+
assert.ok(line.includes("MiniMax"), line);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("writes .harness-status file when pi-lens is not available", () => {
|
|
81
|
+
manager.updateLoopStatus("✍️ coding");
|
|
82
|
+
const statusFile = join(tmpDir, ".harness-status");
|
|
83
|
+
assert.ok(existsSync(statusFile), ".harness-status should exist");
|
|
84
|
+
const content = readFileSync(statusFile, "utf8");
|
|
85
|
+
assert.ok(content.includes("coding"), content);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("start() and stop() do not throw", () => {
|
|
89
|
+
assert.doesNotThrow(() => manager.start());
|
|
90
|
+
assert.doesNotThrow(() => manager.stop());
|
|
91
|
+
});
|
|
92
|
+
});
|