Skip to content

Commit e48a061

Browse files
authored
Merge pull request #7 from Coding-Autopilot-System/feat/autopilot-dashboard
feat(autonomy): Phase 3 Auto-Pilot Dashboard — AUTO-05/06 (249 tests)
2 parents 95be5e9 + 60c1915 commit e48a061

46 files changed

Lines changed: 2129 additions & 165 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.planning/STATE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,52 @@
1+
---
2+
gsd_state_version: 1.0
3+
milestone: v1.0
4+
milestone_name: milestone
5+
status: unknown
6+
last_updated: "2026-06-14T14:22:05.343Z"
7+
last_activity: "2026-06-10 - Completed quick task 260610-pps: Fix PR #1 command documentation blocker."
8+
progress:
9+
total_phases: 3
10+
completed_phases: 0
11+
total_plans: 1
12+
completed_plans: 0
13+
percent: 0
14+
---
15+
116
# Project State: Universal Refiner - Background Autonomy
217

318
## Project Reference
19+
420
**Core Value**: Continuous, background-learning autonomous system for prompt refinement.
521
**Current Focus**: Initializing the Background Autonomy (Auto-Pilot) milestone.
622

723
## Current Position
24+
825
- **Phase**: 1 - Real-time File System Watcher
926
- **Plan**: TBD
1027
- **Status**: Starting milestone
1128
- **Progress**: [░░░░░░░░░░░░░░░░░░░░] 0%
1229

1330
## Performance Metrics
31+
1432
- **Requirement Coverage**: 100% (6/6 mapped)
1533
- **Phase Completion**: 0/3
1634
- **Active Blockers**: None
1735

1836
## Accumulated Context
37+
1938
### Decisions
39+
2040
- Initialized milestone with 3 phases covering FS watching, pipeline automation, and dashboard visibility.
2141
- Chose "Zero-touch" as a core constraint to ensure autonomy.
2242

2343
### Todos
44+
2445
- [ ] Plan Phase 1
2546
- [ ] Research best FS watching library for TypeScript/Node.js in this environment.
2647

2748
### Blockers
49+
2850
- None.
2951

3052
### Quick Tasks Completed
@@ -34,5 +56,6 @@
3456
| 260610-pps | Fix PR #1 review blocker: README and build_and_install.ps1 must accurately document the globally exposed gemini-prompt-refiner command | 2026-06-10 | this commit | Verified | [260610-pps-fix-pr-1-review-blocker-readme-and-build](./quick/260610-pps-fix-pr-1-review-blocker-readme-and-build/) |
3557

3658
## Session Continuity
59+
3760
Last activity: 2026-06-10 - Completed quick task 260610-pps: Fix PR #1 command documentation blocker.
3861
The project is set up with a clear 3-phase roadmap. Next step is to begin planning Phase 1.

universal-refiner/hooks/config/codex.config.fragment.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Codex CLI 0.138.0 does not expose per-prompt pre/post lifecycle hooks.
1+
# Codex CLI does not expose per-prompt pre/post lifecycle hooks.
22
# Keep PromptImprover registered as an MCP server and invoke lint_prompt and
33
# record_agent_output through repo instructions or explicit tool calls.
44
#

universal-refiner/hooks/lib/hook-runtime.ts

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,41 @@ export interface McpToolCaller {
1616
}
1717

1818
const STATE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
19+
const DEFAULT_MAX_STDIN_BYTES = 1024 * 1024;
20+
const READ_CHUNK_BYTES = 64 * 1024;
21+
const TRANSPORT_ERROR_CODES = new Set(["ECONNREFUSED", "ECONNRESET", "EPIPE", "ENOENT"]);
1922

2023
export function parseHookInput(raw: string): HookInput {
2124
const normalized = raw.replace(/^\uFEFF/, "").trim();
2225
return normalized ? JSON.parse(normalized) as HookInput : {};
2326
}
2427

28+
export function readHookInput(descriptor = 0, maxBytes = DEFAULT_MAX_STDIN_BYTES): HookInput {
29+
const limit = Number.isSafeInteger(maxBytes) && maxBytes > 0 ? maxBytes : DEFAULT_MAX_STDIN_BYTES;
30+
const chunks: Buffer[] = [];
31+
let totalBytes = 0;
32+
33+
while (true) {
34+
const chunk = Buffer.alloc(Math.min(READ_CHUNK_BYTES, limit - totalBytes + 1));
35+
const bytesRead = fs.readSync(descriptor, chunk, 0, chunk.length, null);
36+
if (bytesRead === 0) break;
37+
totalBytes += bytesRead;
38+
if (totalBytes > limit) throw Object.assign(new Error("Hook input exceeds the configured limit."), { code: "INPUT_TOO_LARGE" });
39+
chunks.push(chunk.subarray(0, bytesRead));
40+
}
41+
42+
return parseHookInput(Buffer.concat(chunks, totalBytes).toString("utf8"));
43+
}
44+
45+
export function sanitizeError(error: unknown): string {
46+
const code = errorCode(error);
47+
if (code === "INPUT_TOO_LARGE") return "input-too-large";
48+
if (code === -32001 || code === "ETIMEDOUT") return "timeout";
49+
if (code === -32000 || (typeof code === "string" && TRANSPORT_ERROR_CODES.has(code))) return "transport-error";
50+
if (error instanceof SyntaxError) return "invalid-input";
51+
return "hook-error";
52+
}
53+
2554
export function detectClient(input: HookInput): string {
2655
const explicit = stringField(input, "client");
2756
if (explicit) return explicit.toLowerCase();
@@ -96,6 +125,13 @@ export function statePath(input: HookInput): string {
96125
detectClient(input),
97126
stringField(input, "session_id") ?? stringField(input, "sessionId") ?? "no-session",
98127
stringField(input, "cwd") ?? process.cwd(),
128+
firstString(input, [
129+
"request_id", "requestId",
130+
"invocation_id", "invocationId",
131+
"turn_id", "turnId",
132+
"hook_id", "hookId",
133+
"transcript_path", "transcriptPath",
134+
]) ?? "no-hook-id",
99135
].join("|");
100136
const hash = createHash("sha256").update(key).digest("hex");
101137
return path.join(os.tmpdir(), "promptimprover-hooks", `${hash}.json`);
@@ -152,18 +188,21 @@ export async function runPostExecution(input: HookInput, callTool: McpToolCaller
152188

153189
const client = state?.client ?? detectClient(input);
154190
const outputLength = extractOutputLength(input);
155-
await callTool("record_agent_output", {
156-
prompt_id: promptId,
157-
output_summary: `${client} completed the tracked turn; output_length=${outputLength}.`,
158-
artifacts_json: JSON.stringify({
159-
client,
160-
hook_event: stringField(input, "hook_event_name") ?? "manual",
161-
output_length: outputLength,
162-
}),
163-
status: stringField(input, "status") === "failed" ? "failed" : "completed",
164-
});
165-
clearState(input);
166-
return allowOutput(input);
191+
try {
192+
await callTool("record_agent_output", {
193+
prompt_id: promptId,
194+
output_summary: `${client} completed the tracked turn; output_length=${outputLength}.`,
195+
artifacts_json: JSON.stringify({
196+
client,
197+
hook_event: stringField(input, "hook_event_name") ?? "manual",
198+
output_length: outputLength,
199+
}),
200+
status: stringField(input, "status") === "failed" ? "failed" : "completed",
201+
});
202+
return allowOutput(input);
203+
} finally {
204+
clearState(input);
205+
}
167206
}
168207

169208
function firstString(input: HookInput, fields: string[]): string | undefined {
@@ -178,3 +217,9 @@ function stringField(input: HookInput, field: string): string | undefined {
178217
const value = input[field];
179218
return typeof value === "string" ? value : undefined;
180219
}
220+
221+
function errorCode(error: unknown): unknown {
222+
return typeof error === "object" && error !== null && "code" in error
223+
? (error as { code?: unknown }).code
224+
: undefined;
225+
}

universal-refiner/hooks/lib/mcp-client.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
22
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3-
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
3+
import { CallToolResultSchema, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
44
import * as fs from "fs";
55
import * as path from "path";
66
import { fileURLToPath } from "url";
77

88
const DEFAULT_TIMEOUT_MS = 15_000;
9+
const RECONNECT_SAFE_CODES = new Set(["ECONNREFUSED", "ECONNRESET", "EPIPE", "ENOENT"]);
910

1011
export async function callMcpTool(name: string, args: Record<string, unknown>): Promise<string> {
12+
const deadline = Date.now() + timeoutMs();
13+
try {
14+
return await callMcpToolOnce(name, args, deadline);
15+
} catch (error) {
16+
if (!isReconnectSafeTransportFailure(error)) throw error;
17+
return callMcpToolOnce(name, args, deadline);
18+
}
19+
}
20+
21+
async function callMcpToolOnce(name: string, args: Record<string, unknown>, deadline: number): Promise<string> {
22+
remainingMs(deadline);
1123
const transport = new StdioClientTransport({
1224
command: process.execPath,
1325
args: [resolveServerPath()],
@@ -16,17 +28,18 @@ export async function callMcpTool(name: string, args: Record<string, unknown>):
1628
const client = new Client({ name: "promptimprover-cross-cli-hook", version: "1.0.0" }, { capabilities: {} });
1729

1830
try {
19-
await client.connect(transport);
20-
const result = await client.request(
31+
await withinDeadline(client.connect(transport), deadline);
32+
const remaining = remainingMs(deadline);
33+
const result = await withinDeadline(client.request(
2134
{ method: "tools/call", params: { name, arguments: args } },
2235
CallToolResultSchema,
23-
{ timeout: timeoutMs() },
24-
);
36+
{ timeout: remaining, maxTotalTimeout: remaining },
37+
), deadline);
2538
const text = result.content.find((item) => item.type === "text");
26-
if (!text || text.type !== "text") throw new Error(`MCP tool ${name} returned no text content.`);
39+
if (!text) throw new Error(`MCP tool ${name} returned no text content.`);
2740
return text.text;
2841
} finally {
29-
await client.close().catch(() => undefined);
42+
await withinDeadline(client.close(), deadline).catch(() => undefined);
3043
}
3144
}
3245

@@ -46,3 +59,37 @@ function timeoutMs(): number {
4659
const configured = Number(process.env.PROMPTIMPROVER_HOOK_TIMEOUT_MS);
4760
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_TIMEOUT_MS;
4861
}
62+
63+
function remainingMs(deadline: number): number {
64+
const remaining = deadline - Date.now();
65+
if (remaining <= 0) throw timeoutError();
66+
return remaining;
67+
}
68+
69+
async function withinDeadline<T>(operation: Promise<T>, deadline: number): Promise<T> {
70+
const remaining = remainingMs(deadline);
71+
let timer: NodeJS.Timeout;
72+
const timeout = new Promise<T>((_resolve, reject) => {
73+
timer = setTimeout(() => reject(timeoutError()), remaining);
74+
});
75+
try {
76+
return await Promise.race([operation, timeout]);
77+
} finally {
78+
clearTimeout(timer!);
79+
}
80+
}
81+
82+
function isReconnectSafeTransportFailure(error: unknown): boolean {
83+
const code = errorCode(error);
84+
return code === ErrorCode.ConnectionClosed || (typeof code === "string" && RECONNECT_SAFE_CODES.has(code));
85+
}
86+
87+
function errorCode(error: unknown): unknown {
88+
return typeof error === "object" && error !== null && "code" in error
89+
? (error as { code?: unknown }).code
90+
: undefined;
91+
}
92+
93+
function timeoutError(): Error & { code: ErrorCode.RequestTimeout } {
94+
return Object.assign(new Error("MCP hook deadline exceeded."), { code: ErrorCode.RequestTimeout as const });
95+
}

universal-refiner/hooks/post-execution.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
#!/usr/bin/env node
2-
import * as fs from "fs";
32
import { callMcpTool } from "./lib/mcp-client.js";
4-
import { allowOutput, HookInput, parseHookInput, runPostExecution } from "./lib/hook-runtime.js";
3+
import { allowOutput, HookInput, readHookInput, runPostExecution, sanitizeError } from "./lib/hook-runtime.js";
54

65
async function main(): Promise<void> {
76
let input: HookInput = {};
87
try {
9-
input = parseHookInput(fs.readFileSync(0, "utf8"));
8+
input = readHookInput();
109
console.log(JSON.stringify(await runPostExecution(input, callMcpTool)));
1110
} catch (error) {
12-
console.error(`[PromptImprover] Post-execution hook failed open: ${error instanceof Error ? error.message : "unknown error"}`);
11+
console.error(`[PromptImprover] Post-execution hook failed open: ${sanitizeError(error)}`);
1312
console.log(JSON.stringify(allowOutput(input)));
1413
}
1514
}

universal-refiner/hooks/pre-prompt.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
#!/usr/bin/env node
2-
import * as fs from "fs";
32
import { callMcpTool } from "./lib/mcp-client.js";
4-
import { allowOutput, HookInput, parseHookInput, runPrePrompt } from "./lib/hook-runtime.js";
3+
import { allowOutput, HookInput, readHookInput, runPrePrompt, sanitizeError } from "./lib/hook-runtime.js";
54

65
async function main(): Promise<void> {
76
let input: HookInput = {};
87
try {
9-
input = parseHookInput(fs.readFileSync(0, "utf8"));
8+
input = readHookInput();
109
console.log(JSON.stringify(await runPrePrompt(input, callMcpTool)));
1110
} catch (error) {
12-
console.error(`[PromptImprover] Pre-prompt hook failed open: ${error instanceof Error ? error.message : "unknown error"}`);
11+
console.error(`[PromptImprover] Pre-prompt hook failed open: ${sanitizeError(error)}`);
1312
console.log(JSON.stringify(allowOutput(input)));
1413
}
1514
}

universal-refiner/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@
2323
"package:check": "npm pack --dry-run",
2424
"db:backup": "node scripts/operations/event-store-recovery.mjs backup",
2525
"db:restore": "node scripts/operations/event-store-recovery.mjs restore",
26+
"recovery:event-store:abrupt": "node scripts/operations/event-store-abrupt-recovery.mjs",
2627
"acceptance:semantic": "node scripts/acceptance/semantic-provider-acceptance.mjs",
28+
"acceptance:gemma:live": "node scripts/acceptance/semantic-provider-acceptance.mjs --require-live",
29+
"acceptance:tracked-turn": "node scripts/acceptance/tracked-turn-acceptance.mjs",
2730
"stress:event-store": "node scripts/stress/event-store-stress.mjs",
28-
"release:verify": "npm run build && npm run test:coverage && npm run test:acceptance && npm run acceptance:semantic && npm run test:stress && npm run stress:event-store && npm run security:audit && npm run security:secrets && npm run package:check"
31+
"stress:event-store:soak": "node scripts/stress/event-store-soak.mjs",
32+
"release:verify": "npm run build && npm run test:coverage && npm run test:acceptance && npm run acceptance:semantic && npm run acceptance:tracked-turn && npm run test:stress && npm run stress:event-store && npm run recovery:event-store:abrupt && npm run stress:event-store:soak && npm run security:audit && npm run security:secrets && npm run package:check"
2933
},
3034
"keywords": [
3135
"mcp",
Lines changed: 12 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,12 @@
1-
# Global AI Agent Registration Script
2-
# Targets: Claude Desktop, Codex CLI/App, Gemini CLI
3-
4-
$serverCommand = "node"
5-
$serverPath = "C:/repo/Promptimprover/universal-refiner/dist/src/index.js"
6-
7-
# 1. Claude Desktop
8-
$claudePath = "$env:APPDATA\Claude\claude_desktop_config.json"
9-
if (Test-Path $claudePath) {
10-
Write-Host "Registering in Claude Desktop..." -ForegroundColor Cyan
11-
$config = Get-Content $claudePath | ConvertFrom-Json
12-
if (-not $config.mcpServers) { $config | Add-Member -MemberType NoteProperty -Name "mcpServers" -Value @{} }
13-
$config.mcpServers | Add-Member -MemberType NoteProperty -Name "universal-refiner" -Value @{ command = $serverCommand; args = @($serverPath) } -Force
14-
$config | ConvertTo-Json -Depth 10 | Set-Content $claudePath
15-
}
16-
17-
# 2. Codex (config.toml)
18-
$codexPath = "$HOME\.codex\config.toml"
19-
if (Test-Path $codexPath) {
20-
Write-Host "Registering in Codex..." -ForegroundColor Cyan
21-
$content = Get-Content $codexPath -Raw
22-
if ($content -notmatch "\[mcp_servers\.universal-refiner\]") {
23-
$codexEntry = @"
24-
25-
[mcp_servers.universal-refiner]
26-
command = "$serverCommand"
27-
args = ["$serverPath"]
28-
"@
29-
Add-Content $codexPath $codexEntry
30-
}
31-
}
32-
33-
# 3. Gemini CLI (global)
34-
$geminiGlobalDir = "$HOME\.gemini"
35-
if (-not (Test-Path $geminiGlobalDir)) { New-Item -Path $geminiGlobalDir -ItemType Directory -Force }
36-
$geminiConfigPath = "$geminiGlobalDir\gemini-extension.json"
37-
Write-Host "Registering in Global Gemini Config..." -ForegroundColor Cyan
38-
$geminiEntry = @{
39-
name = "universal-refiner"
40-
version = "9.0.0"
41-
mcpServers = @{
42-
"universal-refiner" = @{
43-
command = $serverCommand
44-
args = @($serverPath)
45-
}
46-
}
47-
}
48-
$geminiEntry | ConvertTo-Json -Depth 10 | Set-Content $geminiConfigPath
49-
50-
Write-Host "DONE! Universal Refiner registered globally for Claude, Codex, and Gemini." -ForegroundColor Green
51-
Write-Host "Please restart your AI apps to apply changes." -ForegroundColor Yellow
1+
[CmdletBinding()]
2+
param(
3+
[switch]$Check,
4+
[switch]$Apply,
5+
[string]$ProfileRoot = ("C:\Users\KimHarjam{0}ki" -f [char]0x00E4),
6+
[string]$CodexHome,
7+
[string]$ObsidianVaultPath = "C:\repo\global.obsidian"
8+
)
9+
10+
$operation = Join-Path $PSScriptRoot "scripts\operations\register-global.ps1"
11+
& $operation @PSBoundParameters
12+
exit $LASTEXITCODE

universal-refiner/scripts/acceptance/semantic-provider-acceptance.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,19 @@ import {
88
const primary = process.env.PROMPT_REFINER_PRIMARY_MODEL || "gemma3:12b";
99
const fallback = process.env.PROMPT_REFINER_FALLBACK_MODEL || "gemma3:1b";
1010
const liveBaseUrl = process.env.PROMPT_REFINER_ACCEPTANCE_BASE_URL;
11+
const requireLive = process.argv.includes("--require-live")
12+
|| process.env.PROMPT_REFINER_ACCEPTANCE_REQUIRE_LIVE === "true";
1113
const fake = await startFakeOpenAiServer({
1214
unavailableModels: [primary],
1315
responses: { [fallback]: "fallback accepted" },
1416
});
1517

1618
try {
19+
assert.ok(!requireLive || liveBaseUrl, [
20+
"Required-live Gemma acceptance needs PROMPT_REFINER_ACCEPTANCE_BASE_URL.",
21+
"Set it to the live OpenAI-compatible endpoint that serves the configured Gemma models.",
22+
].join(" "));
23+
1724
if (liveBaseUrl) {
1825
for (const model of [primary, fallback]) {
1926
const liveProvider = new LocalOpenAiProvider({

0 commit comments

Comments
 (0)