pi-orchestrator-config 4.1.5
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 +808 -0
- package/agents/api-documenter.md +32 -0
- package/agents/bash-expert.md +53 -0
- package/agents/code-reviewer-docs.md +145 -0
- package/agents/code-reviewer-guidelines.md +97 -0
- package/agents/code-reviewer-quality.md +142 -0
- package/agents/code-reviewer-security.md +107 -0
- package/agents/code-reviewer-spec.md +164 -0
- package/agents/debugger.md +63 -0
- package/agents/docker-expert.md +40 -0
- package/agents/docs-fetcher.md +48 -0
- package/agents/git-expert.md +63 -0
- package/agents/github-expert.md +83 -0
- package/agents/go-expert.md +62 -0
- package/agents/issue-reviewer-feasibility.md +60 -0
- package/agents/issue-reviewer-scope.md +63 -0
- package/agents/issue-reviewer-spec.md +77 -0
- package/agents/java-expert.md +31 -0
- package/agents/jenkins-expert.md +38 -0
- package/agents/kubernetes-expert.md +40 -0
- package/agents/planner.md +42 -0
- package/agents/python-expert.md +61 -0
- package/agents/reviewer.md +26 -0
- package/agents/scout.md +40 -0
- package/agents/security-auditor.md +254 -0
- package/agents/technical-documentation-writer.md +37 -0
- package/agents/test-automator.md +55 -0
- package/agents/test-runner.md +47 -0
- package/agents/ts-expert.md +23 -0
- package/agents/worker.md +30 -0
- package/extensions/acpx-provider/index.ts +104 -0
- package/extensions/acpx-provider/load-runtime.ts +99 -0
- package/extensions/acpx-provider/runtime-models.ts +40 -0
- package/extensions/cli-provider/agents/claude.ts +128 -0
- package/extensions/cli-provider/agents/cursor.ts +84 -0
- package/extensions/cli-provider/agents/gemini.ts +100 -0
- package/extensions/cli-provider/discover.ts +77 -0
- package/extensions/cli-provider/index.ts +57 -0
- package/extensions/cli-provider/parsers.ts +304 -0
- package/extensions/cli-provider/providers.ts +58 -0
- package/extensions/cli-provider/runner.ts +225 -0
- package/extensions/cli-provider/runtime-models.ts +32 -0
- package/extensions/cli-provider/session-reaper.ts +122 -0
- package/extensions/cli-provider/sessions.ts +533 -0
- package/extensions/cli-provider/shared/discover-cache.ts +235 -0
- package/extensions/cli-provider/types.ts +48 -0
- package/extensions/coms/coms-net-server.ts +1642 -0
- package/extensions/coms/coms-net-wrapper.ts +521 -0
- package/extensions/coms/coms-net.ts +1589 -0
- package/extensions/coms/coms-p2p.ts +1879 -0
- package/extensions/coms/coms-shared.ts +510 -0
- package/extensions/coms/coms-wrapper.ts +138 -0
- package/extensions/coms/index.ts +20 -0
- package/extensions/coms/themeMap.ts +147 -0
- package/extensions/image-gen/image-gen.ts +332 -0
- package/extensions/image-gen/index.ts +14 -0
- package/extensions/orchestrator/agents.ts +134 -0
- package/extensions/orchestrator/ask-user.ts +293 -0
- package/extensions/orchestrator/async-agents.ts +1037 -0
- package/extensions/orchestrator/async-capability.ts +129 -0
- package/extensions/orchestrator/async-runner.ts +324 -0
- package/extensions/orchestrator/async-status-parse.ts +31 -0
- package/extensions/orchestrator/async-status-ui.ts +201 -0
- package/extensions/orchestrator/async-wait.ts +36 -0
- package/extensions/orchestrator/btw.ts +210 -0
- package/extensions/orchestrator/cron-status-format.ts +126 -0
- package/extensions/orchestrator/cron-status-ui.ts +132 -0
- package/extensions/orchestrator/cron.ts +730 -0
- package/extensions/orchestrator/dreaming.ts +355 -0
- package/extensions/orchestrator/enforcement-helpers.ts +530 -0
- package/extensions/orchestrator/enforcement-rules.ts +329 -0
- package/extensions/orchestrator/enforcement.ts +828 -0
- package/extensions/orchestrator/extended-autocomplete.ts +593 -0
- package/extensions/orchestrator/git-helpers.ts +420 -0
- package/extensions/orchestrator/github-autocomplete.ts +250 -0
- package/extensions/orchestrator/icons.ts +17 -0
- package/extensions/orchestrator/index.ts +100 -0
- package/extensions/orchestrator/memory-embeddings.ts +281 -0
- package/extensions/orchestrator/memory-promotion.ts +449 -0
- package/extensions/orchestrator/memory-provenance.ts +80 -0
- package/extensions/orchestrator/memory-query-class.ts +90 -0
- package/extensions/orchestrator/memory-scoring.ts +434 -0
- package/extensions/orchestrator/memory-tools.ts +883 -0
- package/extensions/orchestrator/memory-tree.ts +212 -0
- package/extensions/orchestrator/nvim.ts +169 -0
- package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
- package/extensions/orchestrator/overlay-dashboard.ts +512 -0
- package/extensions/orchestrator/pi-config-review-state.ts +303 -0
- package/extensions/orchestrator/preference-extractor.ts +106 -0
- package/extensions/orchestrator/project-settings.ts +468 -0
- package/extensions/orchestrator/promotion-queue.ts +311 -0
- package/extensions/orchestrator/resolve-agent-model.ts +32 -0
- package/extensions/orchestrator/review-ui.ts +204 -0
- package/extensions/orchestrator/rule-placeholders.ts +12 -0
- package/extensions/orchestrator/rules.ts +481 -0
- package/extensions/orchestrator/session-search.ts +243 -0
- package/extensions/orchestrator/session-validation.ts +374 -0
- package/extensions/orchestrator/situation-report.ts +370 -0
- package/extensions/orchestrator/status-bar.ts +31 -0
- package/extensions/orchestrator/status-line.ts +192 -0
- package/extensions/orchestrator/status.ts +110 -0
- package/extensions/orchestrator/subagent-tool.ts +1748 -0
- package/extensions/orchestrator/task-lifecycle.ts +88 -0
- package/extensions/orchestrator/utils.ts +227 -0
- package/extensions/pidash/index.ts +15 -0
- package/extensions/pidash/pidash.ts +941 -0
- package/extensions/pidiff/index.ts +13 -0
- package/extensions/pidiff/pidiff.ts +369 -0
- package/extensions/providers/acpx-driver.ts +479 -0
- package/extensions/providers/built-in-drivers.ts +52 -0
- package/extensions/providers/claude-driver.ts +319 -0
- package/extensions/providers/cursor-acpx-driver.ts +398 -0
- package/extensions/providers/cursor-cli-driver.ts +484 -0
- package/extensions/providers/gemini-driver.ts +302 -0
- package/extensions/providers/index.ts +513 -0
- package/extensions/shared/build-system-prompt.ts +57 -0
- package/extensions/shared/create-runtime-provider.ts +190 -0
- package/extensions/shared/daemon-manager.ts +248 -0
- package/extensions/shared/file-logger.ts +115 -0
- package/extensions/shared/managed-refresh.ts +211 -0
- package/extensions/shared/provider-driver.ts +295 -0
- package/extensions/shared/provider-errors.ts +305 -0
- package/extensions/shared/provider-registry.ts +388 -0
- package/extensions/shared/stream-builder.ts +275 -0
- package/extensions/shared/types.ts +20 -0
- package/extensions/shared/ui/README.md +10 -0
- package/extensions/shared/ui/badge.tsx +52 -0
- package/extensions/shared/ui/button.tsx +58 -0
- package/extensions/shared/ui/collapsible.tsx +21 -0
- package/extensions/shared/ui/dropdown-menu.tsx +266 -0
- package/extensions/shared/ui/input.tsx +20 -0
- package/extensions/shared/ui/popover.tsx +114 -0
- package/extensions/shared/ui/resizable.tsx +48 -0
- package/extensions/shared/ui/scroll-area.tsx +53 -0
- package/extensions/shared/ui/select.tsx +201 -0
- package/extensions/shared/ui/separator.tsx +25 -0
- package/extensions/shared/ui/tooltip.tsx +64 -0
- package/extensions/shared/ws-client.ts +74 -0
- package/package.json +59 -0
- package/prompts/coderabbit-rate-limit.md +95 -0
- package/prompts/create-coms-feature-manager.md +186 -0
- package/prompts/create-skill.md +128 -0
- package/prompts/domain-model.md +109 -0
- package/prompts/external-ai.md +665 -0
- package/prompts/implement-and-review.md +25 -0
- package/prompts/implement.md +26 -0
- package/prompts/issue-review.md +256 -0
- package/prompts/pr-review.md +753 -0
- package/prompts/query-db.md +154 -0
- package/prompts/refine-review.md +208 -0
- package/prompts/release.md +235 -0
- package/prompts/remember.md +33 -0
- package/prompts/review-handler-status.md +27 -0
- package/prompts/review-handler.md +677 -0
- package/prompts/review-local.md +159 -0
- package/prompts/scout-and-plan.md +21 -0
- package/rules/00-orchestrator-core.md +42 -0
- package/rules/05-issue-first-workflow.md +119 -0
- package/rules/10-agent-routing.md +58 -0
- package/rules/15-mcp-launchpad.md +46 -0
- package/rules/20-code-review-loop.md +234 -0
- package/rules/25-documentation-updates.md +15 -0
- package/rules/30-prompt-templates.md +19 -0
- package/rules/35-memory.md +240 -0
- package/rules/40-critical-rules.md +124 -0
- package/rules/45-file-preview.md +29 -0
- package/rules/50-agent-bug-reporting.md +90 -0
- package/rules/55-coms-protocol.md +63 -0
- package/rules/60-task-tracking.md +66 -0
- package/skills/conflict-resolver/SKILL.md +213 -0
- package/skills/skill-creator/SKILL.md +85 -0
- package/skills/systematic-debugging/SKILL.md +353 -0
- package/skills/test-driven-development/SKILL.md +335 -0
- package/skills/update-agents-md/SKILL.md +282 -0
- package/templates/coms-feature-manager-prompt.md +313 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review state machine — tracks code review loop status.
|
|
3
|
+
* State stored in <worktree-root>/.pi/data/pi-config-review-state.json (per-worktree, not shared).
|
|
4
|
+
* Each worktree gets its own state file via resolveWorktreeRoot (--show-toplevel).
|
|
5
|
+
* Used by enforcement to block git commit until all reviewers approve.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { resolveWorktreeRoot, getProjectDataDir } from "./utils.js";
|
|
11
|
+
|
|
12
|
+
type StateTransitionCallback = (state: ReviewState) => void;
|
|
13
|
+
let onTransitionCb: StateTransitionCallback | null = null;
|
|
14
|
+
|
|
15
|
+
/** Register a callback to be notified on review state transitions.
|
|
16
|
+
* Only one callback is supported — subsequent calls replace the previous one. */
|
|
17
|
+
export function onStateTransition(cb: StateTransitionCallback): void {
|
|
18
|
+
onTransitionCb = cb;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Fire-and-forget state transition notification. Never throws. */
|
|
22
|
+
function notifyTransition(state: ReviewState): void {
|
|
23
|
+
if (!onTransitionCb) return;
|
|
24
|
+
try { onTransitionCb({ ...state, reviewers_pending: [...state.reviewers_pending] }); }
|
|
25
|
+
catch (e: any) { console.debug("[pi-config-review-state] transition callback failed:", e?.message); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReviewState {
|
|
29
|
+
status: "none" | "needs_review" | "in_progress" | "has_findings" | "clean";
|
|
30
|
+
cycle: number;
|
|
31
|
+
reviewers_pending: string[];
|
|
32
|
+
reviewers_total: number;
|
|
33
|
+
findings_count: number;
|
|
34
|
+
last_edit_at: string | null;
|
|
35
|
+
last_clean_at: string | null;
|
|
36
|
+
edited_during_cycle: boolean;
|
|
37
|
+
tests_passed: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const STATE_FILE = "pi-config-review-state.json";
|
|
41
|
+
|
|
42
|
+
export function statePath(cwd: string): string {
|
|
43
|
+
return join(getProjectDataDir(resolveWorktreeRoot(cwd)), STATE_FILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureDataDir(cwd: string): void {
|
|
47
|
+
getProjectDataDir(resolveWorktreeRoot(cwd));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function defaultState(): ReviewState {
|
|
51
|
+
return {
|
|
52
|
+
status: "none",
|
|
53
|
+
cycle: 0,
|
|
54
|
+
reviewers_pending: [],
|
|
55
|
+
reviewers_total: 0,
|
|
56
|
+
findings_count: 0,
|
|
57
|
+
last_edit_at: null,
|
|
58
|
+
last_clean_at: null,
|
|
59
|
+
edited_during_cycle: false,
|
|
60
|
+
tests_passed: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readReviewState(cwd: string): ReviewState {
|
|
65
|
+
const p = statePath(cwd);
|
|
66
|
+
if (!existsSync(p)) return defaultState();
|
|
67
|
+
try {
|
|
68
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
69
|
+
return {
|
|
70
|
+
status: raw.status ?? "none",
|
|
71
|
+
cycle: typeof raw.cycle === "number" ? raw.cycle : 0,
|
|
72
|
+
reviewers_pending: Array.isArray(raw.reviewers_pending) ? raw.reviewers_pending : [],
|
|
73
|
+
reviewers_total: typeof raw.reviewers_total === "number" ? raw.reviewers_total : 0,
|
|
74
|
+
findings_count: typeof raw.findings_count === "number" ? raw.findings_count : 0,
|
|
75
|
+
last_edit_at: typeof raw.last_edit_at === "string" ? raw.last_edit_at : null,
|
|
76
|
+
last_clean_at: typeof raw.last_clean_at === "string" ? raw.last_clean_at : null,
|
|
77
|
+
edited_during_cycle: raw.edited_during_cycle === true,
|
|
78
|
+
tests_passed: raw.tests_passed === true,
|
|
79
|
+
};
|
|
80
|
+
} catch (e: any) {
|
|
81
|
+
console.debug("[pi-config-review-state] failed to parse state:", e?.message);
|
|
82
|
+
return defaultState();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function lockPath(cwd: string): string {
|
|
87
|
+
return statePath(cwd) + ".lock";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function acquireLock(cwd: string): boolean {
|
|
91
|
+
const lock = lockPath(cwd);
|
|
92
|
+
const maxRetries = 100;
|
|
93
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
94
|
+
try {
|
|
95
|
+
ensureDataDir(cwd);
|
|
96
|
+
writeFileSync(lock, String(process.pid), { flag: "wx" });
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
// Lock exists — check if holder is alive
|
|
100
|
+
try {
|
|
101
|
+
const pid = parseInt(readFileSync(lock, "utf-8").trim(), 10);
|
|
102
|
+
// Reentrant: safe because Node.js is single-threaded — nested
|
|
103
|
+
// withStateLock calls within the same process can't interleave.
|
|
104
|
+
if (pid === process.pid) return true;
|
|
105
|
+
if (pid) {
|
|
106
|
+
try { process.kill(pid, 0); } catch {
|
|
107
|
+
// Holder is dead — steal lock
|
|
108
|
+
try { unlinkSync(lock); } catch { /* race */ }
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Corrupt lock — remove and retry
|
|
114
|
+
try { unlinkSync(lock); } catch { /* race */ }
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Holder is alive — bounded tight spin (100 iterations ≈ instant, sub-ms)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function releaseLock(cwd: string): void {
|
|
124
|
+
try { unlinkSync(lockPath(cwd)); } catch { /* ignore */ }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function writeState(cwd: string, state: ReviewState): void {
|
|
128
|
+
const p = statePath(cwd);
|
|
129
|
+
try {
|
|
130
|
+
ensureDataDir(cwd);
|
|
131
|
+
// Atomic write: temp file + rename
|
|
132
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
133
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
|
|
134
|
+
renameSync(tmp, p);
|
|
135
|
+
} catch (e: any) {
|
|
136
|
+
console.debug("[pi-config-review-state] write failed:", e?.message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const lockDepth = new Map<string, number>();
|
|
141
|
+
|
|
142
|
+
/** Execute a read-modify-write operation on the state file with a lock.
|
|
143
|
+
* Throws if lock cannot be acquired (never proceeds without mutual exclusion). */
|
|
144
|
+
function withStateLock<T>(cwd: string, fn: (state: ReviewState) => T): T {
|
|
145
|
+
const key = statePath(cwd);
|
|
146
|
+
const depth = lockDepth.get(key) || 0;
|
|
147
|
+
if (depth === 0) {
|
|
148
|
+
if (!acquireLock(cwd)) {
|
|
149
|
+
throw new Error("[pi-config-review-state] failed to acquire lock — aborting to prevent state corruption");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
lockDepth.set(key, depth + 1);
|
|
153
|
+
try {
|
|
154
|
+
return fn(readReviewState(cwd));
|
|
155
|
+
} finally {
|
|
156
|
+
const newDepth = (lockDepth.get(key) || 1) - 1;
|
|
157
|
+
if (newDepth <= 0) {
|
|
158
|
+
lockDepth.delete(key);
|
|
159
|
+
releaseLock(cwd);
|
|
160
|
+
} else {
|
|
161
|
+
lockDepth.set(key, newDepth);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Mark that files were edited — review is needed. Resets any previous CLEAN state.
|
|
167
|
+
* Does NOT reset during an active review cycle (in_progress) — edits during review
|
|
168
|
+
* are tracked via last_edit_at but don't wipe the pending reviewer list.
|
|
169
|
+
* The `cycle` counter is only reset on a fresh review start (status was "clean"
|
|
170
|
+
* or "none") — continuing from "has_findings"/"needs_review" preserves it so the
|
|
171
|
+
* max-cycles cap is reachable across fix-edit-review iterations. */
|
|
172
|
+
export function markNeedsReview(cwd: string): void {
|
|
173
|
+
withStateLock(cwd, (state) => {
|
|
174
|
+
const prevStatus = state.status;
|
|
175
|
+
const prevTestsPassed = state.tests_passed;
|
|
176
|
+
if (state.status === "in_progress") {
|
|
177
|
+
state.last_edit_at = new Date().toISOString();
|
|
178
|
+
state.edited_during_cycle = true;
|
|
179
|
+
state.tests_passed = false;
|
|
180
|
+
} else {
|
|
181
|
+
// Only a fresh review start (clean or no tracking yet) resets the cycle
|
|
182
|
+
// counter. Continuing from has_findings/needs_review (fix-edit-review loop)
|
|
183
|
+
// must preserve cycle so the max-cycles cap can actually be reached.
|
|
184
|
+
const isFreshStart = state.status === "clean" || state.status === "none";
|
|
185
|
+
state.status = "needs_review";
|
|
186
|
+
state.last_edit_at = new Date().toISOString();
|
|
187
|
+
if (isFreshStart) {
|
|
188
|
+
state.cycle = 0;
|
|
189
|
+
}
|
|
190
|
+
state.findings_count = 0;
|
|
191
|
+
state.reviewers_pending = [];
|
|
192
|
+
state.reviewers_total = 0;
|
|
193
|
+
state.edited_during_cycle = false;
|
|
194
|
+
state.tests_passed = false;
|
|
195
|
+
}
|
|
196
|
+
writeState(cwd, state);
|
|
197
|
+
// Skip notification if nothing meaningful changed (e.g., repeated edits while already needs_review)
|
|
198
|
+
if (state.status !== prevStatus || state.tests_passed !== prevTestsPassed) {
|
|
199
|
+
notifyTransition(state);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Add a single reviewer to the pending list. Sets status to in_progress if not already. */
|
|
205
|
+
export function addReviewerPending(cwd: string, reviewerName: string): void {
|
|
206
|
+
withStateLock(cwd, (state) => {
|
|
207
|
+
if (!state.reviewers_pending.includes(reviewerName)) {
|
|
208
|
+
state.reviewers_pending.push(reviewerName);
|
|
209
|
+
state.reviewers_total = Math.max(state.reviewers_total, state.reviewers_pending.length);
|
|
210
|
+
}
|
|
211
|
+
if (state.status !== "in_progress") {
|
|
212
|
+
state.status = "in_progress";
|
|
213
|
+
state.cycle++;
|
|
214
|
+
state.findings_count = 0;
|
|
215
|
+
state.edited_during_cycle = false;
|
|
216
|
+
}
|
|
217
|
+
writeState(cwd, state);
|
|
218
|
+
notifyTransition(state);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Record a reviewer's result. Idempotent — skips if reviewer already reported. Returns true if all reviewers have reported. */
|
|
223
|
+
export function recordReviewerResult(cwd: string, reviewerName: string, findingsCount: number): boolean {
|
|
224
|
+
return withStateLock(cwd, (state) => {
|
|
225
|
+
if (!state.reviewers_pending.includes(reviewerName)) {
|
|
226
|
+
return state.reviewers_pending.length === 0;
|
|
227
|
+
}
|
|
228
|
+
state.reviewers_pending = state.reviewers_pending.filter(n => n !== reviewerName);
|
|
229
|
+
state.findings_count += findingsCount;
|
|
230
|
+
if (state.reviewers_pending.length === 0) {
|
|
231
|
+
if (state.status === "in_progress") {
|
|
232
|
+
if (state.edited_during_cycle) {
|
|
233
|
+
state.status = "needs_review";
|
|
234
|
+
} else {
|
|
235
|
+
state.status = state.findings_count > 0 ? "has_findings" : "clean";
|
|
236
|
+
if (state.status === "clean") {
|
|
237
|
+
state.last_clean_at = new Date().toISOString();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
writeState(cwd, state);
|
|
243
|
+
notifyTransition(state);
|
|
244
|
+
return state.reviewers_pending.length === 0;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Check if the review state is clean (all reviewers approved, no edits since). */
|
|
249
|
+
export function isReviewClean(cwd: string): boolean {
|
|
250
|
+
const state = readReviewState(cwd);
|
|
251
|
+
if (state.status === "none") return true; // No tracking active — nothing to enforce
|
|
252
|
+
if (state.status !== "clean") return false;
|
|
253
|
+
if (!state.tests_passed) return false;
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Mark that tests have passed. Only meaningful when review status is being tracked. */
|
|
258
|
+
export function markTestsPassed(cwd: string): void {
|
|
259
|
+
withStateLock(cwd, (state) => {
|
|
260
|
+
if (state.status === "none") return; // No tracking active
|
|
261
|
+
state.tests_passed = true;
|
|
262
|
+
writeState(cwd, state);
|
|
263
|
+
notifyTransition(state);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Mark that tests have failed. No-op when review status is not being tracked.
|
|
268
|
+
* Called when a detected test command exits non-zero, or test-automator/test-runner agent fails.
|
|
269
|
+
* Resets to false on any file edit via markNeedsReview(). */
|
|
270
|
+
export function markTestsFailed(cwd: string): void {
|
|
271
|
+
withStateLock(cwd, (state) => {
|
|
272
|
+
if (state.status === "none") return; // No tracking active
|
|
273
|
+
state.tests_passed = false;
|
|
274
|
+
writeState(cwd, state);
|
|
275
|
+
notifyTransition(state);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Count findings in reviewer output by parsing JSON.
|
|
280
|
+
* Returns the number of findings, or -1 if the output is not valid JSON with a findings array. */
|
|
281
|
+
export function countFindings(output: string): number {
|
|
282
|
+
// Try JSON parse — reviewers return {"findings": [...]}
|
|
283
|
+
try {
|
|
284
|
+
// Strip markdown code fences if present
|
|
285
|
+
let cleaned = output.trim();
|
|
286
|
+
if (cleaned.startsWith("```")) {
|
|
287
|
+
cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
288
|
+
}
|
|
289
|
+
const parsed = JSON.parse(cleaned);
|
|
290
|
+
if (parsed && Array.isArray(parsed.findings)) {
|
|
291
|
+
return parsed.findings.length;
|
|
292
|
+
}
|
|
293
|
+
} catch { /* not valid JSON — return -1 */ }
|
|
294
|
+
return -1;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Reset review state — for testing or manual override. */
|
|
298
|
+
export function resetReviewState(cwd: string): void {
|
|
299
|
+
withStateLock(cwd, () => {
|
|
300
|
+
writeState(cwd, defaultState());
|
|
301
|
+
notifyTransition(defaultState());
|
|
302
|
+
});
|
|
303
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preference Auto-Extractor — detects user preferences from conversation.
|
|
3
|
+
*
|
|
4
|
+
* Listens to user input and automatically adds detected preference statements
|
|
5
|
+
* to the preferences topic file. Uses pattern matching to find phrases like
|
|
6
|
+
* "I prefer...", "always use...", "never...", etc.
|
|
7
|
+
*
|
|
8
|
+
* Architecture inspired by OpenHuman (https://github.com/tinyhumansai/openhuman).
|
|
9
|
+
* Clean-room TypeScript implementation under MIT — not a code translation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { extractPreferences, entryHash, loadScores, saveScores } from "./memory-scoring.js";
|
|
16
|
+
import type { ScoredEntry } from "./memory-scoring.js";
|
|
17
|
+
|
|
18
|
+
// Debounce: don't extract from every message, only meaningful ones
|
|
19
|
+
const MIN_MESSAGE_LENGTH = 20;
|
|
20
|
+
|
|
21
|
+
// Cooldown: don't re-extract the same preference within 1 hour
|
|
22
|
+
const COOLDOWN_MS = 60 * 60 * 1000;
|
|
23
|
+
const recentExtractions = new Map<string, number>();
|
|
24
|
+
|
|
25
|
+
export function registerPreferenceExtractor(pi: ExtensionAPI): void {
|
|
26
|
+
// Only run in the orchestrator, not in subagents
|
|
27
|
+
if (process.env.PI_SUBAGENT_CHILD === "1") return;
|
|
28
|
+
|
|
29
|
+
pi.on("input", async (event, ctx) => {
|
|
30
|
+
const text = event.text;
|
|
31
|
+
if (!text || text.length < MIN_MESSAGE_LENGTH) return;
|
|
32
|
+
// Skip slash commands
|
|
33
|
+
if (text.startsWith("/")) return;
|
|
34
|
+
|
|
35
|
+
const preferences = extractPreferences(text);
|
|
36
|
+
if (preferences.length === 0) return;
|
|
37
|
+
|
|
38
|
+
const topicsDir = join(ctx.cwd, ".pi", "memory", "topics");
|
|
39
|
+
const prefPath = join(topicsDir, "preferences.md");
|
|
40
|
+
|
|
41
|
+
// Ensure topics directory exists
|
|
42
|
+
if (!existsSync(topicsDir)) {
|
|
43
|
+
mkdirSync(topicsDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Read existing preferences topic file
|
|
47
|
+
let content = "";
|
|
48
|
+
if (existsSync(prefPath)) {
|
|
49
|
+
content = readFileSync(prefPath, "utf-8");
|
|
50
|
+
} else {
|
|
51
|
+
content = "# Preferences\n\n";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const scores = loadScores(ctx.cwd);
|
|
55
|
+
let added = 0;
|
|
56
|
+
const sourceSession =
|
|
57
|
+
(ctx as any).sessionManager?.getSessionId?.() || undefined;
|
|
58
|
+
|
|
59
|
+
for (const pref of preferences) {
|
|
60
|
+
const entryLine = `- [preference] ${pref}`;
|
|
61
|
+
const hash = entryHash(entryLine);
|
|
62
|
+
|
|
63
|
+
// Cooldown check
|
|
64
|
+
const lastExtracted = recentExtractions.get(hash);
|
|
65
|
+
if (lastExtracted && Date.now() - lastExtracted < COOLDOWN_MS) continue;
|
|
66
|
+
|
|
67
|
+
// Check if already exists
|
|
68
|
+
if (content.includes(pref)) {
|
|
69
|
+
// Reinforce existing entry
|
|
70
|
+
if (scores.entries[hash]) {
|
|
71
|
+
scores.entries[hash]!.evidenceCount += 1;
|
|
72
|
+
scores.entries[hash]!.lastReinforced = new Date().toISOString();
|
|
73
|
+
if (sourceSession && !scores.entries[hash]!.sourceSession) {
|
|
74
|
+
scores.entries[hash]!.sourceSession = sourceSession;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
recentExtractions.set(hash, Date.now());
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Append to preferences topic file
|
|
82
|
+
content = content.trimEnd() + "\n" + entryLine + "\n";
|
|
83
|
+
|
|
84
|
+
// Add score entry
|
|
85
|
+
scores.entries[hash] = {
|
|
86
|
+
class: "preference",
|
|
87
|
+
score: 1.0,
|
|
88
|
+
evidenceCount: 1,
|
|
89
|
+
cue: "explicit",
|
|
90
|
+
firstSeen: new Date().toISOString(),
|
|
91
|
+
lastReinforced: new Date().toISOString(),
|
|
92
|
+
userState: "auto",
|
|
93
|
+
lifecycle: "active",
|
|
94
|
+
...(sourceSession ? { sourceSession } : {}),
|
|
95
|
+
} as ScoredEntry;
|
|
96
|
+
|
|
97
|
+
recentExtractions.set(hash, Date.now());
|
|
98
|
+
added++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (added > 0) {
|
|
102
|
+
writeFileSync(prefPath, content, "utf-8");
|
|
103
|
+
saveScores(ctx.cwd, scores);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|