pi-continuous-learning 0.3.0
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/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/active-instincts.d.ts +4 -0
- package/dist/active-instincts.d.ts.map +1 -0
- package/dist/active-instincts.js +11 -0
- package/dist/active-instincts.js.map +1 -0
- package/dist/agents-md.d.ts +12 -0
- package/dist/agents-md.d.ts.map +1 -0
- package/dist/agents-md.js +23 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/cli/analyze-prompt.d.ts +2 -0
- package/dist/cli/analyze-prompt.d.ts.map +1 -0
- package/dist/cli/analyze-prompt.js +72 -0
- package/dist/cli/analyze-prompt.js.map +1 -0
- package/dist/cli/analyze.d.ts +3 -0
- package/dist/cli/analyze.d.ts.map +1 -0
- package/dist/cli/analyze.js +214 -0
- package/dist/cli/analyze.js.map +1 -0
- package/dist/confidence.d.ts +25 -0
- package/dist/confidence.d.ts.map +1 -0
- package/dist/confidence.js +77 -0
- package/dist/confidence.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +89 -0
- package/dist/config.js.map +1 -0
- package/dist/error-logger.d.ts +34 -0
- package/dist/error-logger.d.ts.map +1 -0
- package/dist/error-logger.js +102 -0
- package/dist/error-logger.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +118 -0
- package/dist/index.js.map +1 -0
- package/dist/instinct-decay.d.ts +39 -0
- package/dist/instinct-decay.d.ts.map +1 -0
- package/dist/instinct-decay.js +93 -0
- package/dist/instinct-decay.js.map +1 -0
- package/dist/instinct-evolve.d.ts +5 -0
- package/dist/instinct-evolve.d.ts.map +1 -0
- package/dist/instinct-evolve.js +24 -0
- package/dist/instinct-evolve.js.map +1 -0
- package/dist/instinct-export.d.ts +40 -0
- package/dist/instinct-export.d.ts.map +1 -0
- package/dist/instinct-export.js +94 -0
- package/dist/instinct-export.js.map +1 -0
- package/dist/instinct-import.d.ts +50 -0
- package/dist/instinct-import.d.ts.map +1 -0
- package/dist/instinct-import.js +168 -0
- package/dist/instinct-import.js.map +1 -0
- package/dist/instinct-injector.d.ts +39 -0
- package/dist/instinct-injector.d.ts.map +1 -0
- package/dist/instinct-injector.js +89 -0
- package/dist/instinct-injector.js.map +1 -0
- package/dist/instinct-loader.d.ts +37 -0
- package/dist/instinct-loader.d.ts.map +1 -0
- package/dist/instinct-loader.js +96 -0
- package/dist/instinct-loader.js.map +1 -0
- package/dist/instinct-parser.d.ts +28 -0
- package/dist/instinct-parser.d.ts.map +1 -0
- package/dist/instinct-parser.js +143 -0
- package/dist/instinct-parser.js.map +1 -0
- package/dist/instinct-projects.d.ts +32 -0
- package/dist/instinct-projects.d.ts.map +1 -0
- package/dist/instinct-projects.js +96 -0
- package/dist/instinct-projects.js.map +1 -0
- package/dist/instinct-promote.d.ts +51 -0
- package/dist/instinct-promote.d.ts.map +1 -0
- package/dist/instinct-promote.js +169 -0
- package/dist/instinct-promote.js.map +1 -0
- package/dist/instinct-status.d.ts +39 -0
- package/dist/instinct-status.d.ts.map +1 -0
- package/dist/instinct-status.js +108 -0
- package/dist/instinct-status.js.map +1 -0
- package/dist/instinct-store.d.ts +30 -0
- package/dist/instinct-store.d.ts.map +1 -0
- package/dist/instinct-store.js +118 -0
- package/dist/instinct-store.js.map +1 -0
- package/dist/instinct-tools.d.ts +161 -0
- package/dist/instinct-tools.d.ts.map +1 -0
- package/dist/instinct-tools.js +240 -0
- package/dist/instinct-tools.js.map +1 -0
- package/dist/observations.d.ts +22 -0
- package/dist/observations.d.ts.map +1 -0
- package/dist/observations.js +62 -0
- package/dist/observations.js.map +1 -0
- package/dist/observer-guard.d.ts +3 -0
- package/dist/observer-guard.d.ts.map +1 -0
- package/dist/observer-guard.js +13 -0
- package/dist/observer-guard.js.map +1 -0
- package/dist/project.d.ts +16 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +59 -0
- package/dist/project.js.map +1 -0
- package/dist/prompt-observer.d.ts +25 -0
- package/dist/prompt-observer.d.ts.map +1 -0
- package/dist/prompt-observer.js +63 -0
- package/dist/prompt-observer.js.map +1 -0
- package/dist/prompts/analyzer-user.d.ts +38 -0
- package/dist/prompts/analyzer-user.d.ts.map +1 -0
- package/dist/prompts/analyzer-user.js +105 -0
- package/dist/prompts/analyzer-user.js.map +1 -0
- package/dist/prompts/evolve-prompt.d.ts +3 -0
- package/dist/prompts/evolve-prompt.d.ts.map +1 -0
- package/dist/prompts/evolve-prompt.js +51 -0
- package/dist/prompts/evolve-prompt.js.map +1 -0
- package/dist/scrubber.d.ts +9 -0
- package/dist/scrubber.d.ts.map +1 -0
- package/dist/scrubber.js +40 -0
- package/dist/scrubber.js.map +1 -0
- package/dist/storage.d.ts +22 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +71 -0
- package/dist/storage.js.map +1 -0
- package/dist/tool-observer.d.ts +32 -0
- package/dist/tool-observer.d.ts.map +1 -0
- package/dist/tool-observer.js +77 -0
- package/dist/tool-observer.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
- package/src/active-instincts.ts +13 -0
- package/src/agents-md.ts +23 -0
- package/src/cli/analyze-prompt.ts +71 -0
- package/src/cli/analyze.ts +286 -0
- package/src/confidence.ts +103 -0
- package/src/config.ts +111 -0
- package/src/error-logger.ts +130 -0
- package/src/index.ts +144 -0
- package/src/instinct-decay.ts +117 -0
- package/src/instinct-evolve.ts +44 -0
- package/src/instinct-export.ts +138 -0
- package/src/instinct-import.ts +260 -0
- package/src/instinct-injector.ts +128 -0
- package/src/instinct-loader.ts +146 -0
- package/src/instinct-parser.ts +171 -0
- package/src/instinct-projects.ts +119 -0
- package/src/instinct-promote.ts +231 -0
- package/src/instinct-status.ts +135 -0
- package/src/instinct-store.ts +149 -0
- package/src/instinct-tools.ts +340 -0
- package/src/observations.ts +82 -0
- package/src/observer-guard.ts +14 -0
- package/src/project.ts +70 -0
- package/src/prompt-observer.ts +92 -0
- package/src/prompts/analyzer-user.ts +156 -0
- package/src/prompts/evolve-prompt.ts +71 -0
- package/src/scrubber.ts +42 -0
- package/src/storage.ts +91 -0
- package/src/tool-observer.ts +114 -0
- package/src/types.ts +90 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool call observation handlers for pi-continuous-learning.
|
|
3
|
+
* Captures tool_execution_start and tool_execution_end events as JSONL observations.
|
|
4
|
+
*/
|
|
5
|
+
import { getCurrentActiveInstincts } from "./active-instincts.js";
|
|
6
|
+
import { appendObservation } from "./observations.js";
|
|
7
|
+
import { shouldSkipObservation } from "./observer-guard.js";
|
|
8
|
+
import { scrubSecrets } from "./scrubber.js";
|
|
9
|
+
import { logError } from "./error-logger.js";
|
|
10
|
+
export const MAX_TOOL_INPUT_LENGTH = 5000;
|
|
11
|
+
export const MAX_TOOL_OUTPUT_LENGTH = 5000;
|
|
12
|
+
function truncate(text, maxLen) {
|
|
13
|
+
if (text.length <= maxLen)
|
|
14
|
+
return text;
|
|
15
|
+
return text.slice(0, maxLen);
|
|
16
|
+
}
|
|
17
|
+
function getSessionId(ctx) {
|
|
18
|
+
return ctx.sessionManager.getSessionId();
|
|
19
|
+
}
|
|
20
|
+
function buildActiveInstincts() {
|
|
21
|
+
const ids = getCurrentActiveInstincts();
|
|
22
|
+
return ids.length > 0 ? { active_instincts: ids } : {};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Handles tool_execution_start events.
|
|
26
|
+
* Records an observation with event: tool_start, tool name, and scrubbed/truncated input.
|
|
27
|
+
*/
|
|
28
|
+
export function handleToolStart(event, ctx, project, baseDir) {
|
|
29
|
+
try {
|
|
30
|
+
if (shouldSkipObservation())
|
|
31
|
+
return;
|
|
32
|
+
const raw = typeof event.args === "string" ? event.args : JSON.stringify(event.args);
|
|
33
|
+
const input = truncate(scrubSecrets(raw), MAX_TOOL_INPUT_LENGTH);
|
|
34
|
+
const observation = {
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
event: "tool_start",
|
|
37
|
+
session: getSessionId(ctx),
|
|
38
|
+
project_id: project.id,
|
|
39
|
+
project_name: project.name,
|
|
40
|
+
tool: event.toolName,
|
|
41
|
+
input,
|
|
42
|
+
...buildActiveInstincts(),
|
|
43
|
+
};
|
|
44
|
+
appendObservation(observation, project.id, baseDir);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
logError(project.id, "tool-observer:handleToolStart", err, baseDir);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Handles tool_execution_end events.
|
|
52
|
+
* Records an observation with event: tool_complete, tool name, scrubbed/truncated output, and is_error.
|
|
53
|
+
*/
|
|
54
|
+
export function handleToolEnd(event, ctx, project, baseDir) {
|
|
55
|
+
try {
|
|
56
|
+
if (shouldSkipObservation())
|
|
57
|
+
return;
|
|
58
|
+
const raw = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
59
|
+
const output = truncate(scrubSecrets(raw), MAX_TOOL_OUTPUT_LENGTH);
|
|
60
|
+
const observation = {
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
event: "tool_complete",
|
|
63
|
+
session: getSessionId(ctx),
|
|
64
|
+
project_id: project.id,
|
|
65
|
+
project_name: project.name,
|
|
66
|
+
tool: event.toolName,
|
|
67
|
+
output,
|
|
68
|
+
is_error: event.isError,
|
|
69
|
+
...buildActiveInstincts(),
|
|
70
|
+
};
|
|
71
|
+
appendObservation(observation, project.id, baseDir);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
logError(project.id, "tool-observer:handleToolEnd", err, baseDir);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=tool-observer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-observer.js","sourceRoot":"","sources":["../src/tool-observer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoBH,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAG7C,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAE3C,SAAS,QAAQ,CAAC,IAAY,EAAE,MAAc;IAC5C,IAAI,IAAI,CAAC,MAAM,IAAI,MAAM;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,YAAY,CAAC,GAAqB;IACzC,OAAO,GAAG,CAAC,cAAc,CAAC,YAAY,EAAE,CAAC;AAC3C,CAAC;AAED,SAAS,oBAAoB;IAC3B,MAAM,GAAG,GAAG,yBAAyB,EAAE,CAAC;IACxC,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACzD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,KAA8B,EAC9B,GAAqB,EACrB,OAAqB,EACrB,OAAgB;IAEhB,IAAI,CAAC;QACH,IAAI,qBAAqB,EAAE;YAAE,OAAO;QAEpC,MAAM,GAAG,GAAG,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACrF,MAAM,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,qBAAqB,CAAC,CAAC;QAEjE,MAAM,WAAW,GAAgB;YAC/B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,YAAY,CAAC,GAAG,CAAC;YAC1B,UAAU,EAAE,OAAO,CAAC,EAAE;YACtB,YAAY,EAAE,OAAO,CAAC,IAAI;YAC1B,IAAI,EAAE,KAAK,CAAC,QAAQ;YACpB,KAAK;YACL,GAAG,oBAAoB,EAAE;SAC1B,CAAC;QAEF,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,QAAQ,CAAC,OAAO,CAAC,EAAE,EAAE,+BAA+B,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;IACtE,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,KAA4B,EAC5B,GAAqB,EACrB,OAAqB,EACrB,OAAgB;IAEhB,IAAI,CAAC;QACH,IAAI,qBAAqB,EAAE;YAAE,OAAO;QAEpC,MAAM,GAAG,GACP,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACjF,MAAM,MAAM,GAAG,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,sBAAsB,CAAC,CAAC;QAEnE,MAAM,WAAW,GAAgB;YAC/B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,KAAK,EAAE,eAAe;YACtB,OAAO,EAAE,YAAY,CAAC,GAAG,CAAC;YAC1B,UAAU,EAAE,OAAO,CAAC,EAAE;YACtB,YAAY,EAAE,OAAO,CAAC,IAAI;YAC1B,IAAI,EAAE,KAAK,CAAC,QAAQ;YACpB,MAAM;YACN,QAAQ,EAAE,KAAK,CAAC,OAAO;YACvB,GAAG,oBAAoB,EAAE;SAC1B,CAAC;QAEF,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,QAAQ,CAAC,OAAO,CAAC,EAAE,EAAE,6BAA6B,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;IACpE,CAAC;AACH,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TypeScript interfaces for pi-continuous-learning.
|
|
3
|
+
* All modules import from this file for consistent data contracts.
|
|
4
|
+
*/
|
|
5
|
+
export type ObservationEvent = "tool_start" | "tool_complete" | "user_prompt" | "agent_end";
|
|
6
|
+
export interface Observation {
|
|
7
|
+
timestamp: string;
|
|
8
|
+
event: ObservationEvent;
|
|
9
|
+
session: string;
|
|
10
|
+
project_id: string;
|
|
11
|
+
project_name: string;
|
|
12
|
+
tool?: string;
|
|
13
|
+
input?: string;
|
|
14
|
+
output?: string;
|
|
15
|
+
is_error?: boolean;
|
|
16
|
+
active_instincts?: string[];
|
|
17
|
+
}
|
|
18
|
+
export type InstinctScope = "project" | "global";
|
|
19
|
+
export type InstinctSource = "personal" | "inherited";
|
|
20
|
+
export interface Instinct {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
trigger: string;
|
|
24
|
+
action: string;
|
|
25
|
+
confidence: number;
|
|
26
|
+
domain: string;
|
|
27
|
+
source: InstinctSource;
|
|
28
|
+
scope: InstinctScope;
|
|
29
|
+
project_id?: string;
|
|
30
|
+
project_name?: string;
|
|
31
|
+
created_at: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
observation_count: number;
|
|
34
|
+
confirmed_count: number;
|
|
35
|
+
contradicted_count: number;
|
|
36
|
+
inactive_count: number;
|
|
37
|
+
evidence?: string[];
|
|
38
|
+
flagged_for_removal?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface ProjectEntry {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
root: string;
|
|
44
|
+
remote: string;
|
|
45
|
+
created_at: string;
|
|
46
|
+
last_seen: string;
|
|
47
|
+
}
|
|
48
|
+
export interface InstalledSkill {
|
|
49
|
+
name: string;
|
|
50
|
+
description: string;
|
|
51
|
+
}
|
|
52
|
+
export interface Config {
|
|
53
|
+
run_interval_minutes: number;
|
|
54
|
+
min_observations_to_analyze: number;
|
|
55
|
+
min_confidence: number;
|
|
56
|
+
max_instincts: number;
|
|
57
|
+
max_injection_chars: number;
|
|
58
|
+
model: string;
|
|
59
|
+
timeout_seconds: number;
|
|
60
|
+
active_hours_start: number;
|
|
61
|
+
active_hours_end: number;
|
|
62
|
+
max_idle_seconds: number;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,MAAM,gBAAgB,GACxB,YAAY,GACZ,eAAe,GACf,aAAa,GACb,WAAW,CAAC;AAEhB,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAMD,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,QAAQ,CAAC;AACjD,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;AAEtD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,aAAa,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAMD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,MAAM;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,2BAA2B,EAAE,MAAM,CAAC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC1B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-continuous-learning",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "A Pi extension that observes coding sessions and distills patterns into reusable instincts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Matt Devy",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/MattDevy/pi-continuous-learning.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/MattDevy/pi-continuous-learning#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/MattDevy/pi-continuous-learning/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi-extension",
|
|
19
|
+
"pi-coding-agent",
|
|
20
|
+
"continuous-learning",
|
|
21
|
+
"instincts"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src",
|
|
29
|
+
"!src/**/*.test.ts",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"pi": {
|
|
36
|
+
"extensions": [
|
|
37
|
+
"dist/index.js"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"bin": {
|
|
41
|
+
"pi-cl-analyze": "dist/cli/analyze.js"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"clean": "rm -rf dist",
|
|
45
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"lint": "eslint src/",
|
|
49
|
+
"check": "vitest run && eslint src/ && tsc --noEmit",
|
|
50
|
+
"prepublishOnly": "npm run build && npm run check"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
54
|
+
"@mariozechner/pi-ai": "^0.62.0",
|
|
55
|
+
"@mariozechner/pi-tui": "^0.62.0",
|
|
56
|
+
"@sinclair/typebox": "^0.34.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^24.0.0",
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
61
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
62
|
+
"eslint": "^9.0.0",
|
|
63
|
+
"typescript": "^5.7.0",
|
|
64
|
+
"vitest": "^3.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
let activeInstincts: string[] = [];
|
|
2
|
+
|
|
3
|
+
export function getCurrentActiveInstincts(): string[] {
|
|
4
|
+
return [...activeInstincts];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function setCurrentActiveInstincts(ids: string[]): void {
|
|
8
|
+
activeInstincts = [...ids];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function clearActiveInstincts(): void {
|
|
12
|
+
activeInstincts = [];
|
|
13
|
+
}
|
package/src/agents-md.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for reading AGENTS.md files.
|
|
3
|
+
* Provides a safe wrapper around filesystem access that returns null on any failure.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads an AGENTS.md file and returns its content.
|
|
10
|
+
* Returns null if the file does not exist or cannot be read.
|
|
11
|
+
*
|
|
12
|
+
* @param filePath - Absolute path to the AGENTS.md file
|
|
13
|
+
*/
|
|
14
|
+
export function readAgentsMd(filePath: string): string | null {
|
|
15
|
+
if (!existsSync(filePath)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return readFileSync(filePath, "utf-8");
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function buildAnalyzerSystemPrompt(): string {
|
|
2
|
+
return `You are a coding behavior analyst. Your job is to read session observations
|
|
3
|
+
and produce or update instinct files that capture reusable coding patterns.
|
|
4
|
+
|
|
5
|
+
Use the instinct_read tool to examine existing instincts and the instinct_write tool
|
|
6
|
+
to create or update instincts based on patterns you discover.
|
|
7
|
+
|
|
8
|
+
## Pattern Detection Heuristics
|
|
9
|
+
|
|
10
|
+
Analyze observations for these categories:
|
|
11
|
+
|
|
12
|
+
### User Corrections
|
|
13
|
+
- User rephrases a request after an agent response
|
|
14
|
+
- User explicitly rejects an approach
|
|
15
|
+
- Trigger: the corrected behavior; Action: the preferred approach
|
|
16
|
+
|
|
17
|
+
### Error Resolutions
|
|
18
|
+
- Tool call returns is_error: true followed by a successful retry
|
|
19
|
+
- Trigger: the error condition; Action: the proven resolution
|
|
20
|
+
|
|
21
|
+
### Repeated Workflows
|
|
22
|
+
- Same sequence of tool calls appears 3+ times
|
|
23
|
+
- Trigger: the workflow start condition; Action: the efficient path
|
|
24
|
+
|
|
25
|
+
### Tool Preferences
|
|
26
|
+
- Agent consistently uses one tool over alternatives
|
|
27
|
+
- Trigger: the task type; Action: the preferred tool and parameters
|
|
28
|
+
|
|
29
|
+
### Anti-Patterns
|
|
30
|
+
- Actions that consistently lead to errors or user corrections
|
|
31
|
+
- Trigger: the bad pattern situation; Action: what to do instead
|
|
32
|
+
|
|
33
|
+
## Feedback Analysis
|
|
34
|
+
|
|
35
|
+
Each observation may include an active_instincts field listing instinct IDs
|
|
36
|
+
that were injected into the agent's system prompt before that turn.
|
|
37
|
+
|
|
38
|
+
Use this to update existing instinct confidence scores:
|
|
39
|
+
- Confirmed (+0.05): instinct was active and agent followed the guidance without correction
|
|
40
|
+
- Contradicted (-0.15): instinct was active but user corrected the agent
|
|
41
|
+
- Inactive (no change): instinct was injected but trigger never arose
|
|
42
|
+
|
|
43
|
+
When updating, increment the corresponding count field and recalculate confidence.
|
|
44
|
+
|
|
45
|
+
## Confidence Scoring Rules
|
|
46
|
+
|
|
47
|
+
### Initial Confidence (new instincts)
|
|
48
|
+
- 1-2 observations -> 0.3
|
|
49
|
+
- 3-5 observations -> 0.5
|
|
50
|
+
- 6-10 observations -> 0.7
|
|
51
|
+
- 11+ observations -> 0.85
|
|
52
|
+
|
|
53
|
+
### Clamping
|
|
54
|
+
- Always clamp to [0.1, 0.9]
|
|
55
|
+
|
|
56
|
+
## Scope Decision Guide
|
|
57
|
+
|
|
58
|
+
Use project scope when the pattern is specific to this project's tech stack or conventions.
|
|
59
|
+
Use global scope when the pattern applies universally to any coding session.
|
|
60
|
+
When in doubt, prefer project scope.
|
|
61
|
+
|
|
62
|
+
## Conservativeness Rules
|
|
63
|
+
|
|
64
|
+
1. Only create a new instinct with 3+ clear independent observations supporting the pattern.
|
|
65
|
+
2. No code snippets in the action field - plain language only.
|
|
66
|
+
3. Each instinct must have one well-defined trigger.
|
|
67
|
+
4. New instincts from observation data alone are capped at 0.85 confidence.
|
|
68
|
+
5. Before creating, use instinct_list to check for duplicates. Update existing instincts instead.
|
|
69
|
+
6. Write actions as clear instructions starting with a verb.
|
|
70
|
+
7. Be skeptical of outliers - patterns seen only in unusual circumstances should not become instincts.`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
statSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
createAgentSession,
|
|
12
|
+
SessionManager,
|
|
13
|
+
AuthStorage,
|
|
14
|
+
ModelRegistry,
|
|
15
|
+
DefaultResourceLoader,
|
|
16
|
+
} from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
18
|
+
|
|
19
|
+
import { loadConfig, DEFAULT_CONFIG } from "../config.js";
|
|
20
|
+
import type { ProjectEntry } from "../types.js";
|
|
21
|
+
import {
|
|
22
|
+
getBaseDir,
|
|
23
|
+
getProjectsRegistryPath,
|
|
24
|
+
getObservationsPath,
|
|
25
|
+
getProjectDir,
|
|
26
|
+
} from "../storage.js";
|
|
27
|
+
import { countObservations } from "../observations.js";
|
|
28
|
+
import { runDecayPass } from "../instinct-decay.js";
|
|
29
|
+
import { buildAnalyzerUserPrompt, tailObservationsSince } from "../prompts/analyzer-user.js";
|
|
30
|
+
import { buildAnalyzerSystemPrompt } from "./analyze-prompt.js";
|
|
31
|
+
import {
|
|
32
|
+
createInstinctListTool,
|
|
33
|
+
createInstinctReadTool,
|
|
34
|
+
createInstinctWriteTool,
|
|
35
|
+
createInstinctDeleteTool,
|
|
36
|
+
} from "../instinct-tools.js";
|
|
37
|
+
import { readAgentsMd } from "../agents-md.js";
|
|
38
|
+
import { homedir } from "node:os";
|
|
39
|
+
import type { InstalledSkill } from "../types.js";
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Lockfile guard — ensures only one instance runs at a time
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const LOCKFILE_NAME = "analyze.lock";
|
|
46
|
+
const LOCK_STALE_MS = 10 * 60 * 1000; // 10 minutes — stale lock threshold
|
|
47
|
+
|
|
48
|
+
function getLockfilePath(baseDir: string): string {
|
|
49
|
+
return join(baseDir, LOCKFILE_NAME);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function acquireLock(baseDir: string): boolean {
|
|
53
|
+
const lockPath = getLockfilePath(baseDir);
|
|
54
|
+
|
|
55
|
+
if (existsSync(lockPath)) {
|
|
56
|
+
try {
|
|
57
|
+
const content = readFileSync(lockPath, "utf-8");
|
|
58
|
+
const lock = JSON.parse(content) as { pid: number; started_at: string };
|
|
59
|
+
const age = Date.now() - new Date(lock.started_at).getTime();
|
|
60
|
+
|
|
61
|
+
// Check if the owning process is still alive
|
|
62
|
+
try {
|
|
63
|
+
process.kill(lock.pid, 0); // signal 0 = existence check, no actual signal
|
|
64
|
+
if (age < LOCK_STALE_MS) {
|
|
65
|
+
return false; // Process alive and lock is fresh
|
|
66
|
+
}
|
|
67
|
+
// Process alive but lock is stale — treat as abandoned
|
|
68
|
+
} catch {
|
|
69
|
+
// Process is dead — lock is orphaned, safe to take over
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Malformed lockfile — remove and proceed
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
writeFileSync(
|
|
77
|
+
lockPath,
|
|
78
|
+
JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }),
|
|
79
|
+
"utf-8"
|
|
80
|
+
);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function releaseLock(baseDir: string): void {
|
|
85
|
+
const lockPath = getLockfilePath(baseDir);
|
|
86
|
+
try {
|
|
87
|
+
if (existsSync(lockPath)) unlinkSync(lockPath);
|
|
88
|
+
} catch {
|
|
89
|
+
// Best effort — don't crash on cleanup
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Global timeout
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes total
|
|
98
|
+
|
|
99
|
+
function startGlobalTimeout(timeoutMs: number): void {
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
console.error("[analyze] Global timeout reached. Exiting.");
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}, timeoutMs).unref();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Per-project analysis
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
interface ProjectMeta {
|
|
111
|
+
last_analyzed_at?: string;
|
|
112
|
+
last_observation_line_count?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadProjectsRegistry(baseDir: string): Record<string, ProjectEntry> {
|
|
116
|
+
const path = getProjectsRegistryPath(baseDir);
|
|
117
|
+
if (!existsSync(path)) return {};
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(readFileSync(path, "utf-8")) as Record<string, ProjectEntry>;
|
|
120
|
+
} catch {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function loadProjectMeta(projectId: string, baseDir: string): ProjectMeta {
|
|
126
|
+
const metaPath = join(getProjectDir(projectId, baseDir), "project.json");
|
|
127
|
+
if (!existsSync(metaPath)) return {};
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(readFileSync(metaPath, "utf-8")) as ProjectMeta;
|
|
130
|
+
} catch {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function saveProjectMeta(projectId: string, meta: ProjectMeta, baseDir: string): void {
|
|
136
|
+
const metaPath = join(getProjectDir(projectId, baseDir), "project.json");
|
|
137
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hasNewObservations(projectId: string, meta: ProjectMeta, baseDir: string): boolean {
|
|
141
|
+
const obsPath = getObservationsPath(projectId, baseDir);
|
|
142
|
+
if (!existsSync(obsPath)) return false;
|
|
143
|
+
|
|
144
|
+
const stat = statSync(obsPath);
|
|
145
|
+
if (meta.last_analyzed_at) {
|
|
146
|
+
const lastAnalyzed = new Date(meta.last_analyzed_at).getTime();
|
|
147
|
+
if (stat.mtimeMs <= lastAnalyzed) return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function analyzeProject(
|
|
154
|
+
project: ProjectEntry,
|
|
155
|
+
config: ReturnType<typeof loadConfig>,
|
|
156
|
+
baseDir: string
|
|
157
|
+
): Promise<boolean> {
|
|
158
|
+
const meta = loadProjectMeta(project.id, baseDir);
|
|
159
|
+
|
|
160
|
+
if (!hasNewObservations(project.id, meta, baseDir)) return false;
|
|
161
|
+
|
|
162
|
+
const obsPath = getObservationsPath(project.id, baseDir);
|
|
163
|
+
const sinceLineCount = meta.last_observation_line_count ?? 0;
|
|
164
|
+
const { lines: newObsLines, totalLineCount } = tailObservationsSince(obsPath, sinceLineCount);
|
|
165
|
+
|
|
166
|
+
if (newObsLines.length === 0) return false;
|
|
167
|
+
|
|
168
|
+
const obsCount = countObservations(project.id, baseDir);
|
|
169
|
+
if (obsCount < config.min_observations_to_analyze) return false;
|
|
170
|
+
|
|
171
|
+
console.log(
|
|
172
|
+
`[analyze] Processing ${project.name} (${project.id}): ${newObsLines.length} new observations (${obsCount} total)`
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
runDecayPass(project.id, baseDir);
|
|
176
|
+
|
|
177
|
+
const instinctsDir = join(getProjectDir(project.id, baseDir), "instincts", "personal");
|
|
178
|
+
|
|
179
|
+
const agentsMdProject = readAgentsMd(join(project.root, "AGENTS.md"));
|
|
180
|
+
const agentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
|
|
181
|
+
|
|
182
|
+
let installedSkills: InstalledSkill[] = [];
|
|
183
|
+
try {
|
|
184
|
+
const { loadSkills } = await import("@mariozechner/pi-coding-agent");
|
|
185
|
+
const result = loadSkills({ cwd: project.root });
|
|
186
|
+
installedSkills = result.skills.map((s: { name: string; description: string }) => ({
|
|
187
|
+
name: s.name,
|
|
188
|
+
description: s.description,
|
|
189
|
+
}));
|
|
190
|
+
} catch {
|
|
191
|
+
// Skills loading is best-effort — continue without them
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const userPrompt = buildAnalyzerUserPrompt(obsPath, instinctsDir, project, {
|
|
195
|
+
agentsMdProject,
|
|
196
|
+
agentsMdGlobal,
|
|
197
|
+
installedSkills,
|
|
198
|
+
observationLines: newObsLines,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const authStorage = AuthStorage.create();
|
|
202
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
203
|
+
const modelId = (config.model || DEFAULT_CONFIG.model) as Parameters<typeof getModel>[1];
|
|
204
|
+
const model = getModel("anthropic", modelId);
|
|
205
|
+
|
|
206
|
+
const customTools = [
|
|
207
|
+
createInstinctListTool(project.id, baseDir),
|
|
208
|
+
createInstinctReadTool(project.id, baseDir),
|
|
209
|
+
createInstinctWriteTool(project.id, project.name, baseDir),
|
|
210
|
+
createInstinctDeleteTool(project.id, baseDir),
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const loader = new DefaultResourceLoader({
|
|
214
|
+
systemPromptOverride: () => buildAnalyzerSystemPrompt(),
|
|
215
|
+
});
|
|
216
|
+
await loader.reload();
|
|
217
|
+
|
|
218
|
+
const { session } = await createAgentSession({
|
|
219
|
+
model,
|
|
220
|
+
authStorage,
|
|
221
|
+
modelRegistry,
|
|
222
|
+
sessionManager: SessionManager.inMemory(),
|
|
223
|
+
customTools,
|
|
224
|
+
resourceLoader: loader,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await session.prompt(userPrompt);
|
|
229
|
+
} finally {
|
|
230
|
+
session.dispose();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
saveProjectMeta(
|
|
234
|
+
project.id,
|
|
235
|
+
{ ...meta, last_analyzed_at: new Date().toISOString(), last_observation_line_count: totalLineCount },
|
|
236
|
+
baseDir
|
|
237
|
+
);
|
|
238
|
+
console.log(`[analyze] Completed ${project.name}`);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Main
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
async function main(): Promise<void> {
|
|
247
|
+
const baseDir = getBaseDir();
|
|
248
|
+
|
|
249
|
+
if (!acquireLock(baseDir)) {
|
|
250
|
+
console.log("[analyze] Another instance is already running. Exiting.");
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
startGlobalTimeout(DEFAULT_TIMEOUT_MS);
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const config = loadConfig();
|
|
258
|
+
const registry = loadProjectsRegistry(baseDir);
|
|
259
|
+
const projects = Object.values(registry);
|
|
260
|
+
|
|
261
|
+
if (projects.length === 0) {
|
|
262
|
+
console.log("[analyze] No projects registered. Use pi with the continuous-learning extension first.");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let processed = 0;
|
|
267
|
+
for (const project of projects) {
|
|
268
|
+
try {
|
|
269
|
+
const didRun = await analyzeProject(project, config, baseDir);
|
|
270
|
+
if (didRun) processed++;
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error(`[analyze] Error processing ${project.name}: ${String(err)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log(`[analyze] Done. Processed ${processed}/${projects.length} project(s).`);
|
|
277
|
+
} finally {
|
|
278
|
+
releaseLock(baseDir);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
main().catch((err) => {
|
|
283
|
+
releaseLock(getBaseDir());
|
|
284
|
+
console.error(`[analyze] Fatal error: ${String(err)}`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
});
|