pi-crew 0.5.1 → 0.5.2
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/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/docs/actions-reference.md +87 -0
- package/docs/commands-reference.md +5 -0
- package/docs/pi-crew-bugs.md +6 -0
- package/index.ts +1 -1
- package/package.json +18 -16
- package/src/benchmark/benchmark-runner.ts +245 -0
- package/src/benchmark/feedback-loop.ts +66 -0
- package/src/extension/async-notifier.ts +1 -1
- package/src/extension/autonomous-policy.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +1 -1
- package/src/extension/plan-orchestrate.ts +322 -0
- package/src/extension/register.ts +31 -41
- package/src/extension/registration/command-utils.ts +1 -1
- package/src/extension/registration/commands.ts +1 -1
- package/src/extension/registration/compaction-guard.ts +1 -1
- package/src/extension/registration/subagent-helpers.ts +1 -1
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/registration/team-tool.ts +1 -1
- package/src/extension/registration/viewers.ts +1 -1
- package/src/extension/session-summary.ts +1 -1
- package/src/extension/team-manager-command.ts +1 -1
- package/src/extension/team-tool/context.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +183 -0
- package/src/extension/team-tool/orchestrate.ts +102 -0
- package/src/extension/team-tool/run.ts +215 -28
- package/src/extension/team-tool.ts +10 -0
- package/src/extension/tool-result.ts +1 -1
- package/src/i18n.ts +1 -1
- package/src/observability/event-to-metric.ts +1 -1
- package/src/prompt/prompt-runtime.ts +1 -1
- package/src/runtime/background-runner.ts +27 -5
- package/src/runtime/crash-recovery.ts +1 -1
- package/src/runtime/crew-hooks.ts +240 -0
- package/src/runtime/custom-tools/irc-tool.ts +1 -1
- package/src/runtime/custom-tools/submit-result-tool.ts +1 -1
- package/src/runtime/diagnostic-export.ts +38 -2
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/live-session-runtime.ts +1 -1
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/pi-spawn.ts +20 -4
- package/src/runtime/process-status.ts +15 -2
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/task-runner.ts +31 -1
- package/src/runtime/team-runner.ts +6 -0
- package/src/schema/team-tool-schema.ts +24 -1
- package/src/state/crew-init.ts +56 -38
- package/src/state/decision-ledger.ts +295 -0
- package/src/state/hook-instinct-bridge.ts +90 -0
- package/src/state/hook-integrations.ts +51 -0
- package/src/state/instinct-store.ts +249 -0
- package/src/state/run-metrics.ts +135 -0
- package/src/state/tiered-eval.ts +471 -0
- package/src/state/types-eval.ts +58 -0
- package/src/state/types.ts +3 -0
- package/src/tools/safe-bash-extension.ts +5 -5
- package/src/ui/crew-widget.ts +1 -1
- package/src/ui/pi-ui-compat.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +1 -1
- package/src/ui/tool-render.ts +2 -2
- package/src/utils/project-detector.ts +160 -0
- package/test-bugs-all.mjs +1 -1
- package/skills/.gitkeep +0 -0
- package/skills/REFERENCE.md +0 -136
|
@@ -65,6 +65,9 @@ export const TeamToolParams = Type.Object({
|
|
|
65
65
|
Type.Literal("cache"),
|
|
66
66
|
Type.Literal("checkpoint"),
|
|
67
67
|
Type.Literal("search"),
|
|
68
|
+
Type.Literal("orchestrate"),
|
|
69
|
+
Type.Literal("schedule"),
|
|
70
|
+
Type.Literal("scheduled"),
|
|
68
71
|
],
|
|
69
72
|
{ description: "Team action. Defaults to 'list' when omitted." },
|
|
70
73
|
),
|
|
@@ -189,6 +192,18 @@ export const TeamToolParams = Type.Object({
|
|
|
189
192
|
replyDeadline: Type.Optional(
|
|
190
193
|
Type.Integer({ description: "Ms epoch deadline for a reply." }),
|
|
191
194
|
),
|
|
195
|
+
planPath: Type.Optional(
|
|
196
|
+
Type.String({ description: "Path to a markdown plan document for orchestration." }),
|
|
197
|
+
),
|
|
198
|
+
cron: Type.Optional(
|
|
199
|
+
Type.String({ description: "Cron expression for recurring scheduled runs (e.g., '0 9 * * MON')." }),
|
|
200
|
+
),
|
|
201
|
+
interval: Type.Optional(
|
|
202
|
+
Type.Number({ description: "Interval in milliseconds between recurring scheduled runs." }),
|
|
203
|
+
),
|
|
204
|
+
once: Type.Optional(
|
|
205
|
+
Type.Union([Type.String(), Type.Number()], { description: "ISO timestamp or epoch ms for a one-time scheduled run." }),
|
|
206
|
+
),
|
|
192
207
|
});
|
|
193
208
|
|
|
194
209
|
export interface TeamToolParamsValue {
|
|
@@ -234,7 +249,10 @@ export interface TeamToolParamsValue {
|
|
|
234
249
|
| "explain"
|
|
235
250
|
| "cache"
|
|
236
251
|
| "checkpoint"
|
|
237
|
-
| "search"
|
|
252
|
+
| "search"
|
|
253
|
+
| "orchestrate"
|
|
254
|
+
| "schedule"
|
|
255
|
+
| "scheduled";
|
|
238
256
|
resource?: "agent" | "team" | "workflow";
|
|
239
257
|
team?: string;
|
|
240
258
|
workflow?: string;
|
|
@@ -264,4 +282,9 @@ export interface TeamToolParamsValue {
|
|
|
264
282
|
replyFrom?: string;
|
|
265
283
|
/** Ms epoch deadline for a reply. */
|
|
266
284
|
replyDeadline?: number;
|
|
285
|
+
/** Path to a markdown plan document for orchestration. */
|
|
286
|
+
planPath?: string;
|
|
287
|
+
cron?: string;
|
|
288
|
+
interval?: number;
|
|
289
|
+
once?: string | number;
|
|
267
290
|
}
|
package/src/state/crew-init.ts
CHANGED
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
* Auto-initialize .crew directory structure and .gitignore entries.
|
|
3
3
|
* Called on first team run in a workspace to ensure all required
|
|
4
4
|
* directories and files exist.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: This module must be COMPLETELY self-contained with NO dependencies
|
|
7
|
+
* on other pi-crew modules (especially paths.ts). It is called via dynamic
|
|
8
|
+
* import from child-process contexts (background runners, subagents) where
|
|
9
|
+
* module binding can fail. Keep this file minimal and self-contained.
|
|
5
10
|
*/
|
|
6
11
|
import * as fs from "node:fs";
|
|
7
12
|
import * as path from "node:path";
|
|
8
|
-
import { projectCrewRoot } from "../utils/paths.ts";
|
|
9
13
|
import { updateGitignore } from "./gitignore-manager.ts";
|
|
10
14
|
|
|
15
|
+
// Re-export updateGitignore for backwards compatibility with tests.
|
|
16
|
+
export { updateGitignore };
|
|
17
|
+
|
|
11
18
|
/** README content for the .crew directory. */
|
|
12
19
|
const CREW_README = `# .crew — pi-crew Runtime Directory
|
|
13
20
|
|
|
@@ -37,15 +44,57 @@ team action='cache' action='clear'
|
|
|
37
44
|
\`\`\`
|
|
38
45
|
`;
|
|
39
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Find the project root by walking up from start directory.
|
|
49
|
+
* Inline implementation to avoid module dependency on paths.ts.
|
|
50
|
+
* Matches the logic in src/utils/paths.ts:computeRepoRoot().
|
|
51
|
+
*/
|
|
52
|
+
function findProjectRoot(start: string): string | undefined {
|
|
53
|
+
const dirMarkers = [".git", ".hg", ".svn"];
|
|
54
|
+
const fileMarkers = ["package.json", "pyproject.toml", "Cargo.toml", "go.mod"];
|
|
55
|
+
const root = path.parse(start).root;
|
|
56
|
+
let current = path.resolve(start);
|
|
57
|
+
// Walk up to find project root
|
|
58
|
+
while (current !== root) {
|
|
59
|
+
for (const marker of dirMarkers) {
|
|
60
|
+
if (fs.existsSync(path.join(current, marker))) return current;
|
|
61
|
+
}
|
|
62
|
+
for (const marker of fileMarkers) {
|
|
63
|
+
if (fs.existsSync(path.join(current, marker))) return current;
|
|
64
|
+
}
|
|
65
|
+
const parent = path.dirname(current);
|
|
66
|
+
if (parent === current) break;
|
|
67
|
+
current = parent;
|
|
68
|
+
}
|
|
69
|
+
// Check root as fallback
|
|
70
|
+
if (dirMarkers.some((m) => fs.existsSync(path.join(root, m)))) return root;
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compute the crew root directory for a given working directory.
|
|
76
|
+
* Matches src/utils/paths.ts:projectCrewRoot() logic.
|
|
77
|
+
*/
|
|
78
|
+
function computeCrewRoot(cwd: string): string {
|
|
79
|
+
const repoRoot = findProjectRoot(cwd) ?? cwd;
|
|
80
|
+
const crewDir = path.join(repoRoot, ".crew");
|
|
81
|
+
// Keep existing .crew/ stable even when .pi/ exists for project config.
|
|
82
|
+
if (fs.existsSync(crewDir)) return crewDir;
|
|
83
|
+
// Legacy reuse: if .pi/ already exists, namespace under .pi/teams/
|
|
84
|
+
const piDir = path.join(repoRoot, ".pi");
|
|
85
|
+
return fs.existsSync(piDir) ? path.join(piDir, "teams") : crewDir;
|
|
86
|
+
}
|
|
87
|
+
|
|
40
88
|
/**
|
|
41
89
|
* Ensure the .crew directory structure exists with all required subdirectories,
|
|
42
90
|
* placeholder files, README, and .gitignore entries.
|
|
43
91
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
92
|
+
* This function is self-contained with NO dependencies on other pi-crew modules.
|
|
93
|
+
* It uses inline implementations of findProjectRoot and computeCrewRoot to avoid
|
|
94
|
+
* module binding issues in child-process contexts.
|
|
46
95
|
*/
|
|
47
96
|
export async function ensureCrewDirectory(cwd: string): Promise<void> {
|
|
48
|
-
const crewRoot =
|
|
97
|
+
const crewRoot = computeCrewRoot(cwd);
|
|
49
98
|
|
|
50
99
|
// 1. Create directory structure
|
|
51
100
|
const dirs = [
|
|
@@ -81,41 +130,10 @@ export async function ensureCrewDirectory(cwd: string): Promise<void> {
|
|
|
81
130
|
// 3. Write README.md (always overwrite to keep it current)
|
|
82
131
|
fs.writeFileSync(path.join(crewRoot, "README.md"), CREW_README, "utf-8");
|
|
83
132
|
|
|
84
|
-
// 4. Update .gitignore
|
|
85
|
-
|
|
86
|
-
const repoRoot = findRepoRootForGitignore(cwd);
|
|
133
|
+
// 4. Update .gitignore at project root
|
|
134
|
+
const repoRoot = findProjectRoot(cwd);
|
|
87
135
|
if (repoRoot) {
|
|
88
136
|
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
89
137
|
await updateGitignore(gitignorePath);
|
|
90
138
|
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Find the appropriate project root for placing the .gitignore.
|
|
95
|
-
* Walks up from cwd to find a directory with project markers.
|
|
96
|
-
*/
|
|
97
|
-
function findRepoRootForGitignore(cwd: string): string | undefined {
|
|
98
|
-
// Use the same project root markers as paths.ts
|
|
99
|
-
const dirMarkers = [".git", ".pi", ".crew", ".hg", ".svn"];
|
|
100
|
-
const fileMarkers = [
|
|
101
|
-
"package.json",
|
|
102
|
-
"pyproject.toml",
|
|
103
|
-
"Cargo.toml",
|
|
104
|
-
"go.mod",
|
|
105
|
-
];
|
|
106
|
-
const root = path.parse(cwd).root;
|
|
107
|
-
let current = path.resolve(cwd);
|
|
108
|
-
while (current !== root) {
|
|
109
|
-
for (const marker of dirMarkers) {
|
|
110
|
-
if (fs.existsSync(path.join(current, marker))) return current;
|
|
111
|
-
}
|
|
112
|
-
for (const marker of fileMarkers) {
|
|
113
|
-
if (fs.existsSync(path.join(current, marker))) return current;
|
|
114
|
-
}
|
|
115
|
-
const parent = path.dirname(current);
|
|
116
|
-
if (parent === current) break;
|
|
117
|
-
current = parent;
|
|
118
|
-
}
|
|
119
|
-
// No project root found — don't create .gitignore
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
|
|
4
|
+
export interface CoherenceMark {
|
|
5
|
+
matchesPrior: boolean;
|
|
6
|
+
matchesRecursive: boolean;
|
|
7
|
+
promotionAllowed: boolean;
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RolloutEntry {
|
|
12
|
+
rolloutId: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
priorWinner?: string;
|
|
15
|
+
searchSpace: string;
|
|
16
|
+
trialCount: number;
|
|
17
|
+
topCandidates: string[];
|
|
18
|
+
decisionMark: "accept" | "watch" | "reject" | "decay";
|
|
19
|
+
coherenceMark: CoherenceMark;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get the ledger file path for a given run ID.
|
|
24
|
+
*/
|
|
25
|
+
function getLedgerPath(runId: string): string {
|
|
26
|
+
return `.crew/state/runs/${runId}/decision-ledger.jsonl`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Compute coherence marks based on existing ledger entries.
|
|
31
|
+
*/
|
|
32
|
+
function computeCoherence(entry: RolloutEntry, ledger: RolloutEntry[]): CoherenceMark {
|
|
33
|
+
if (ledger.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
matchesPrior: false,
|
|
36
|
+
matchesRecursive: false,
|
|
37
|
+
promotionAllowed: true,
|
|
38
|
+
reason: "No prior entries - first rollout, promotion allowed",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const previousEntry = ledger[ledger.length - 1];
|
|
43
|
+
const matchesPrior: boolean =
|
|
44
|
+
entry.decisionMark === previousEntry.decisionMark ||
|
|
45
|
+
Boolean(entry.priorWinner && entry.topCandidates.includes(entry.priorWinner));
|
|
46
|
+
|
|
47
|
+
// Check last 3 entries for recursive pattern
|
|
48
|
+
const recentEntries = ledger.slice(-3);
|
|
49
|
+
const recentDecisions = recentEntries.map((e) => e.decisionMark);
|
|
50
|
+
const currentDecision = entry.decisionMark;
|
|
51
|
+
|
|
52
|
+
const recursiveMatches = recentDecisions.filter((d) => d === currentDecision).length;
|
|
53
|
+
const matchesRecursive = recursiveMatches >= 2;
|
|
54
|
+
|
|
55
|
+
const promotionAllowed = matchesPrior || matchesRecursive;
|
|
56
|
+
|
|
57
|
+
let reason: string;
|
|
58
|
+
if (matchesPrior && matchesRecursive) {
|
|
59
|
+
reason = `Matches prior winner and recursive pattern (${recursiveMatches}/3 recent decisions)`;
|
|
60
|
+
} else if (matchesPrior) {
|
|
61
|
+
reason = `Matches prior winner decision`;
|
|
62
|
+
} else if (matchesRecursive) {
|
|
63
|
+
reason = `Matches recursive pattern (${recursiveMatches}/3 recent decisions)`;
|
|
64
|
+
} else {
|
|
65
|
+
reason = `No match with prior or recursive pattern - requires human review`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
matchesPrior,
|
|
70
|
+
matchesRecursive,
|
|
71
|
+
promotionAllowed,
|
|
72
|
+
reason,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initialize a new decision ledger for a run.
|
|
78
|
+
* Creates the directory and ledger file if they don't exist.
|
|
79
|
+
*/
|
|
80
|
+
export function initLedger(runId: string): void {
|
|
81
|
+
const ledgerPath = getLedgerPath(runId);
|
|
82
|
+
const dir = dirname(ledgerPath);
|
|
83
|
+
|
|
84
|
+
if (!existsSync(dir)) {
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create empty file if it doesn't exist
|
|
89
|
+
if (!existsSync(ledgerPath)) {
|
|
90
|
+
writeFileSync(ledgerPath, "", "utf-8");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Append a new entry to the decision ledger.
|
|
96
|
+
* Automatically computes and adds coherence marks.
|
|
97
|
+
*/
|
|
98
|
+
export function appendEntry(runId: string, entry: RolloutEntry): RolloutEntry {
|
|
99
|
+
const ledgerPath = getLedgerPath(runId);
|
|
100
|
+
|
|
101
|
+
// Ensure directory exists
|
|
102
|
+
const dir = dirname(ledgerPath);
|
|
103
|
+
if (!existsSync(dir)) {
|
|
104
|
+
mkdirSync(dir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get existing entries to compute coherence
|
|
108
|
+
const ledger = getLedger(runId);
|
|
109
|
+
|
|
110
|
+
// Compute coherence marks
|
|
111
|
+
const coherenceMark = computeCoherence(entry, ledger);
|
|
112
|
+
const entryWithCoherence: RolloutEntry = {
|
|
113
|
+
...entry,
|
|
114
|
+
coherenceMark,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Append to JSONL file
|
|
118
|
+
const line = JSON.stringify(entryWithCoherence) + "\n";
|
|
119
|
+
writeFileSync(ledgerPath, line, { flag: "a", encoding: "utf-8" });
|
|
120
|
+
return entryWithCoherence;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read all entries from the decision ledger.
|
|
125
|
+
*/
|
|
126
|
+
export function getLedger(runId: string): RolloutEntry[] {
|
|
127
|
+
const ledgerPath = getLedgerPath(runId);
|
|
128
|
+
|
|
129
|
+
if (!existsSync(ledgerPath)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const content = readFileSync(ledgerPath, "utf-8");
|
|
134
|
+
if (!content.trim()) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return content
|
|
139
|
+
.split("\n")
|
|
140
|
+
.filter((line) => line.trim())
|
|
141
|
+
.map((line) => JSON.parse(line) as RolloutEntry);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the most recent entry from the decision ledger.
|
|
146
|
+
*/
|
|
147
|
+
export function getLatestDecision(runId: string): RolloutEntry | null {
|
|
148
|
+
const ledger = getLedger(runId);
|
|
149
|
+
if (ledger.length === 0) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return ledger[ledger.length - 1];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate a human-readable markdown summary of the ledger.
|
|
157
|
+
*/
|
|
158
|
+
export function summarizeLedger(runId: string): string {
|
|
159
|
+
const ledger = getLedger(runId);
|
|
160
|
+
|
|
161
|
+
if (ledger.length === 0) {
|
|
162
|
+
return "# Decision Ledger Summary\n\n*No entries recorded yet.*";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const lines: string[] = [
|
|
166
|
+
"# Decision Ledger Summary",
|
|
167
|
+
"",
|
|
168
|
+
`Run ID: ${runId}`,
|
|
169
|
+
`Total Entries: ${ledger.length}`,
|
|
170
|
+
"",
|
|
171
|
+
"## Entries",
|
|
172
|
+
"",
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < ledger.length; i++) {
|
|
176
|
+
const entry = ledger[i];
|
|
177
|
+
lines.push(`### ${i + 1}. ${entry.rolloutId}`);
|
|
178
|
+
lines.push("");
|
|
179
|
+
lines.push(`- **Timestamp**: ${entry.timestamp}`);
|
|
180
|
+
lines.push(`- **Search Space**: ${entry.searchSpace}`);
|
|
181
|
+
lines.push(`- **Trial Count**: ${entry.trialCount}`);
|
|
182
|
+
lines.push(`- **Decision**: ${entry.decisionMark}`);
|
|
183
|
+
|
|
184
|
+
if (entry.priorWinner) {
|
|
185
|
+
lines.push(`- **Prior Winner**: ${entry.priorWinner}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
lines.push(`- **Top Candidates**: ${entry.topCandidates.join(", ") || "(none)"}`);
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push("#### Coherence");
|
|
191
|
+
lines.push(`- **Matches Prior**: ${entry.coherenceMark.matchesPrior ? "✓" : "✗"}`);
|
|
192
|
+
lines.push(`- **Matches Recursive**: ${entry.coherenceMark.matchesRecursive ? "✓" : "✗"}`);
|
|
193
|
+
lines.push(`- **Promotion Allowed**: ${entry.coherenceMark.promotionAllowed ? "✓" : "✗"}`);
|
|
194
|
+
lines.push(`- **Reason**: ${entry.coherenceMark.reason}`);
|
|
195
|
+
lines.push("");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Summary statistics
|
|
199
|
+
const decisions = ledger.map((e) => e.decisionMark);
|
|
200
|
+
const acceptCount = decisions.filter((d) => d === "accept").length;
|
|
201
|
+
const watchCount = decisions.filter((d) => d === "watch").length;
|
|
202
|
+
const rejectCount = decisions.filter((d) => d === "reject").length;
|
|
203
|
+
const decayCount = decisions.filter((d) => d === "decay").length;
|
|
204
|
+
|
|
205
|
+
lines.push("## Summary");
|
|
206
|
+
lines.push("");
|
|
207
|
+
lines.push(`| Decision | Count |`);
|
|
208
|
+
lines.push(`|----------|-------|`);
|
|
209
|
+
lines.push(`| Accept | ${acceptCount} |`);
|
|
210
|
+
lines.push(`| Watch | ${watchCount} |`);
|
|
211
|
+
lines.push(`| Reject | ${rejectCount} |`);
|
|
212
|
+
lines.push(`| Decay | ${decayCount} |`);
|
|
213
|
+
lines.push("");
|
|
214
|
+
|
|
215
|
+
const promotedCount = ledger.filter((e) => e.coherenceMark.promotionAllowed).length;
|
|
216
|
+
lines.push(`**Promotion Rate**: ${promotedCount}/${ledger.length} (${((promotedCount / ledger.length) * 100).toFixed(1)}%)`);
|
|
217
|
+
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Promote a candidate by marking it as accepted with proper coherence.
|
|
223
|
+
*/
|
|
224
|
+
export function promoteCandidate(runId: string, candidate: string): RolloutEntry {
|
|
225
|
+
const latestDecision = getLatestDecision(runId);
|
|
226
|
+
|
|
227
|
+
const entry: RolloutEntry = {
|
|
228
|
+
rolloutId: `promote-${Date.now()}`,
|
|
229
|
+
timestamp: new Date().toISOString(),
|
|
230
|
+
priorWinner: latestDecision?.topCandidates[0],
|
|
231
|
+
searchSpace: latestDecision?.searchSpace || "unknown",
|
|
232
|
+
trialCount: (latestDecision?.trialCount || 0) + 1,
|
|
233
|
+
topCandidates: [candidate],
|
|
234
|
+
decisionMark: "accept",
|
|
235
|
+
coherenceMark: {
|
|
236
|
+
matchesPrior: false,
|
|
237
|
+
matchesRecursive: false,
|
|
238
|
+
promotionAllowed: true,
|
|
239
|
+
reason: "Manual promotion by user",
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Persist via appendEntry so ledger is consistent.
|
|
244
|
+
appendEntry(runId, entry);
|
|
245
|
+
const manualCoherence: import("./types.js").CoherenceMark = {
|
|
246
|
+
matchesPrior: false,
|
|
247
|
+
matchesRecursive: false,
|
|
248
|
+
promotionAllowed: true,
|
|
249
|
+
reason: "Manual promotion by user",
|
|
250
|
+
};
|
|
251
|
+
// Manually override the last line in the JSONL to reflect the coherent
|
|
252
|
+
// decision we want, bypassing appendEntry's auto-compute for the returned value.
|
|
253
|
+
const lastLine = readFileSync(getLedgerPath(runId), "utf-8").trim().split("\n").filter(Boolean).at(-1)!;
|
|
254
|
+
const overridden: RolloutEntry = { ...JSON.parse(lastLine), coherenceMark: manualCoherence };
|
|
255
|
+
writeFileSync(getLedgerPath(runId), JSON.stringify(overridden) + "\n", "utf-8");
|
|
256
|
+
return overridden;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Decay a candidate by marking it as decayed with proper coherence.
|
|
261
|
+
*/
|
|
262
|
+
export function decayCandidate(runId: string, candidate: string): RolloutEntry {
|
|
263
|
+
const latestDecision = getLatestDecision(runId);
|
|
264
|
+
|
|
265
|
+
const entry: RolloutEntry = {
|
|
266
|
+
rolloutId: `decay-${Date.now()}`,
|
|
267
|
+
timestamp: new Date().toISOString(),
|
|
268
|
+
priorWinner: latestDecision?.topCandidates[0],
|
|
269
|
+
searchSpace: latestDecision?.searchSpace || "unknown",
|
|
270
|
+
trialCount: (latestDecision?.trialCount || 0) + 1,
|
|
271
|
+
topCandidates: [candidate],
|
|
272
|
+
decisionMark: "decay",
|
|
273
|
+
coherenceMark: {
|
|
274
|
+
matchesPrior: false,
|
|
275
|
+
matchesRecursive: false,
|
|
276
|
+
promotionAllowed: false,
|
|
277
|
+
reason: "Manual decay by user",
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Persist via appendEntry so ledger is consistent.
|
|
282
|
+
appendEntry(runId, entry);
|
|
283
|
+
const manualCoherence: import("./types.js").CoherenceMark = {
|
|
284
|
+
matchesPrior: false,
|
|
285
|
+
matchesRecursive: false,
|
|
286
|
+
promotionAllowed: false,
|
|
287
|
+
reason: "Manual decay by user",
|
|
288
|
+
};
|
|
289
|
+
// Manually override the last line in the JSONL to reflect the coherent
|
|
290
|
+
// decision we want, bypassing appendEntry's auto-compute for the returned value.
|
|
291
|
+
const lastLine = readFileSync(getLedgerPath(runId), "utf-8").trim().split("\n").filter(Boolean).at(-1)!;
|
|
292
|
+
const overridden: RolloutEntry = { ...JSON.parse(lastLine), coherenceMark: manualCoherence };
|
|
293
|
+
writeFileSync(getLedgerPath(runId), JSON.stringify(overridden) + "\n", "utf-8");
|
|
294
|
+
return overridden;
|
|
295
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook-to-instinct bridge - connects crewHooks events to instinct formation.
|
|
3
|
+
* Auto-initializes when imported.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { crewHooks } from "../runtime/crew-hooks.ts";
|
|
7
|
+
|
|
8
|
+
// Lazy-initialized store and paths
|
|
9
|
+
let storeInstance: import("./instinct-store").InstinctStore | null = null;
|
|
10
|
+
let pathsInstance: typeof import("../utils/paths") | null = null;
|
|
11
|
+
|
|
12
|
+
async function getStore() {
|
|
13
|
+
if (!storeInstance) {
|
|
14
|
+
const { InstinctStore } = await import("./instinct-store");
|
|
15
|
+
const paths = await import("../utils/paths");
|
|
16
|
+
storeInstance = new InstinctStore(paths.projectCrewRoot(process.cwd()));
|
|
17
|
+
}
|
|
18
|
+
return storeInstance;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function getPaths() {
|
|
22
|
+
if (!pathsInstance) {
|
|
23
|
+
pathsInstance = await import("../utils/paths");
|
|
24
|
+
}
|
|
25
|
+
return pathsInstance;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Subscribe to events
|
|
29
|
+
crewHooks.register("task_completed", async (event) => {
|
|
30
|
+
try {
|
|
31
|
+
const store = await getStore();
|
|
32
|
+
if (event.data?.role) {
|
|
33
|
+
store.saveInstinct({
|
|
34
|
+
trigger: `role:${event.data.role}`,
|
|
35
|
+
action: "prefer",
|
|
36
|
+
confidence: 0.6,
|
|
37
|
+
scope: "global",
|
|
38
|
+
evidence: [`task:${event.taskId} completed`],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Best-effort - don't crash on instinct formation failures
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
crewHooks.register("task_failed", async (event) => {
|
|
47
|
+
try {
|
|
48
|
+
const store = await getStore();
|
|
49
|
+
if (event.data?.role) {
|
|
50
|
+
store.saveInstinct({
|
|
51
|
+
trigger: `role:${event.data.role}`,
|
|
52
|
+
action: "avoid",
|
|
53
|
+
confidence: 0.3,
|
|
54
|
+
scope: "global",
|
|
55
|
+
evidence: [`task:${event.taskId} failed`],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Best-effort
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
crewHooks.register("run_completed", async (event) => {
|
|
64
|
+
try {
|
|
65
|
+
const store = await getStore();
|
|
66
|
+
if (event.data?.taskCount) {
|
|
67
|
+
store.saveInstinct({
|
|
68
|
+
trigger: "run_completed",
|
|
69
|
+
action: `completed:${event.data.taskCount}tasks`,
|
|
70
|
+
confidence: 0.6,
|
|
71
|
+
scope: "global",
|
|
72
|
+
evidence: [`run:${event.runId}`],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// Best-effort
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get instinct-based recommendations.
|
|
82
|
+
*/
|
|
83
|
+
export async function getInstinctRecommendations() {
|
|
84
|
+
try {
|
|
85
|
+
const store = await getStore();
|
|
86
|
+
return store.getInstincts().filter((i: { confidence: number }) => i.confidence >= 0.6);
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook integrations - subscribes to crewHooks and provides observability.
|
|
3
|
+
* Auto-initializes when imported.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { crewHooks } from "../runtime/crew-hooks.ts";
|
|
7
|
+
|
|
8
|
+
// Statistics
|
|
9
|
+
let tasksCompleted = 0;
|
|
10
|
+
let tasksFailed = 0;
|
|
11
|
+
let runsCompleted = 0;
|
|
12
|
+
let runsFailed = 0;
|
|
13
|
+
|
|
14
|
+
// Subscribe to events (fire-and-forget)
|
|
15
|
+
crewHooks.register("task_completed", () => {
|
|
16
|
+
tasksCompleted++;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
crewHooks.register("task_failed", () => {
|
|
20
|
+
tasksFailed++;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
crewHooks.register("run_completed", () => {
|
|
24
|
+
runsCompleted++;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
crewHooks.register("run_failed", () => {
|
|
28
|
+
runsFailed++;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get current hook statistics.
|
|
33
|
+
*/
|
|
34
|
+
export function getHookStats(): {
|
|
35
|
+
tasksCompleted: number;
|
|
36
|
+
tasksFailed: number;
|
|
37
|
+
runsCompleted: number;
|
|
38
|
+
runsFailed: number;
|
|
39
|
+
} {
|
|
40
|
+
return { tasksCompleted, tasksFailed, runsCompleted, runsFailed };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reset statistics (useful for testing).
|
|
45
|
+
*/
|
|
46
|
+
export function resetHookStats(): void {
|
|
47
|
+
tasksCompleted = 0;
|
|
48
|
+
tasksFailed = 0;
|
|
49
|
+
runsCompleted = 0;
|
|
50
|
+
runsFailed = 0;
|
|
51
|
+
}
|