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.
Files changed (66) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/docs/actions-reference.md +87 -0
  4. package/docs/commands-reference.md +5 -0
  5. package/docs/pi-crew-bugs.md +6 -0
  6. package/index.ts +1 -1
  7. package/package.json +18 -16
  8. package/src/benchmark/benchmark-runner.ts +245 -0
  9. package/src/benchmark/feedback-loop.ts +66 -0
  10. package/src/extension/async-notifier.ts +1 -1
  11. package/src/extension/autonomous-policy.ts +1 -1
  12. package/src/extension/cross-extension-rpc.ts +1 -1
  13. package/src/extension/plan-orchestrate.ts +322 -0
  14. package/src/extension/register.ts +31 -41
  15. package/src/extension/registration/command-utils.ts +1 -1
  16. package/src/extension/registration/commands.ts +1 -1
  17. package/src/extension/registration/compaction-guard.ts +1 -1
  18. package/src/extension/registration/subagent-helpers.ts +1 -1
  19. package/src/extension/registration/subagent-tools.ts +1 -1
  20. package/src/extension/registration/team-tool.ts +1 -1
  21. package/src/extension/registration/viewers.ts +1 -1
  22. package/src/extension/session-summary.ts +1 -1
  23. package/src/extension/team-manager-command.ts +1 -1
  24. package/src/extension/team-tool/context.ts +1 -1
  25. package/src/extension/team-tool/handle-schedule.ts +183 -0
  26. package/src/extension/team-tool/orchestrate.ts +102 -0
  27. package/src/extension/team-tool/run.ts +215 -28
  28. package/src/extension/team-tool.ts +10 -0
  29. package/src/extension/tool-result.ts +1 -1
  30. package/src/i18n.ts +1 -1
  31. package/src/observability/event-to-metric.ts +1 -1
  32. package/src/prompt/prompt-runtime.ts +1 -1
  33. package/src/runtime/background-runner.ts +27 -5
  34. package/src/runtime/crash-recovery.ts +1 -1
  35. package/src/runtime/crew-hooks.ts +240 -0
  36. package/src/runtime/custom-tools/irc-tool.ts +1 -1
  37. package/src/runtime/custom-tools/submit-result-tool.ts +1 -1
  38. package/src/runtime/diagnostic-export.ts +38 -2
  39. package/src/runtime/foreground-watchdog.ts +1 -1
  40. package/src/runtime/live-session-runtime.ts +1 -1
  41. package/src/runtime/mcp-proxy.ts +1 -1
  42. package/src/runtime/pi-spawn.ts +20 -4
  43. package/src/runtime/process-status.ts +15 -2
  44. package/src/runtime/runtime-resolver.ts +1 -1
  45. package/src/runtime/session-resources.ts +1 -1
  46. package/src/runtime/task-runner.ts +31 -1
  47. package/src/runtime/team-runner.ts +6 -0
  48. package/src/schema/team-tool-schema.ts +24 -1
  49. package/src/state/crew-init.ts +56 -38
  50. package/src/state/decision-ledger.ts +295 -0
  51. package/src/state/hook-instinct-bridge.ts +90 -0
  52. package/src/state/hook-integrations.ts +51 -0
  53. package/src/state/instinct-store.ts +249 -0
  54. package/src/state/run-metrics.ts +135 -0
  55. package/src/state/tiered-eval.ts +471 -0
  56. package/src/state/types-eval.ts +58 -0
  57. package/src/state/types.ts +3 -0
  58. package/src/tools/safe-bash-extension.ts +5 -5
  59. package/src/ui/crew-widget.ts +1 -1
  60. package/src/ui/pi-ui-compat.ts +1 -1
  61. package/src/ui/run-action-dispatcher.ts +1 -1
  62. package/src/ui/tool-render.ts +2 -2
  63. package/src/utils/project-detector.ts +160 -0
  64. package/test-bugs-all.mjs +1 -1
  65. package/skills/.gitkeep +0 -0
  66. 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
  }
@@ -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
- * Uses `projectCrewRoot()` to resolve the correct root (`.crew/` or `.pi/teams/`
45
- * for legacy projects). Idempotent safe to call multiple times.
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 = projectCrewRoot(cwd);
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 resolve project root to place .gitignore correctly
85
- // Find the repo root to place .gitignore at the project root (not inside .crew)
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
+ }