@kodrunhq/opencode-autopilot 1.15.2 → 1.17.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/bin/cli.ts +5 -0
- package/bin/inspect.ts +337 -0
- package/package.json +1 -1
- package/src/agents/autopilot.ts +7 -15
- package/src/config/index.ts +29 -0
- package/src/config/migrations.ts +196 -0
- package/src/config/v7.ts +45 -0
- package/src/config.ts +3 -3
- package/src/health/checks.ts +126 -4
- package/src/health/types.ts +1 -1
- package/src/index.ts +128 -13
- package/src/inspect/formatters.ts +225 -0
- package/src/inspect/repository.ts +882 -0
- package/src/kernel/database.ts +45 -0
- package/src/kernel/migrations.ts +62 -0
- package/src/kernel/repository.ts +571 -0
- package/src/kernel/schema.ts +122 -0
- package/src/kernel/transaction.ts +48 -0
- package/src/kernel/types.ts +65 -0
- package/src/logging/domains.ts +39 -0
- package/src/logging/forensic-writer.ts +177 -0
- package/src/logging/index.ts +4 -0
- package/src/logging/logger.ts +44 -0
- package/src/logging/performance.ts +59 -0
- package/src/logging/rotation.ts +261 -0
- package/src/logging/types.ts +33 -0
- package/src/memory/capture-utils.ts +149 -0
- package/src/memory/capture.ts +82 -67
- package/src/memory/database.ts +74 -12
- package/src/memory/decay.ts +11 -2
- package/src/memory/index.ts +17 -1
- package/src/memory/injector.ts +4 -1
- package/src/memory/lessons.ts +85 -0
- package/src/memory/observations.ts +177 -0
- package/src/memory/preferences.ts +718 -0
- package/src/memory/project-key.ts +6 -0
- package/src/memory/projects.ts +83 -0
- package/src/memory/repository.ts +52 -216
- package/src/memory/retrieval.ts +88 -170
- package/src/memory/schemas.ts +39 -7
- package/src/memory/types.ts +4 -0
- package/src/observability/context-display.ts +8 -0
- package/src/observability/event-handlers.ts +69 -20
- package/src/observability/event-store.ts +29 -1
- package/src/observability/forensic-log.ts +167 -0
- package/src/observability/forensic-schemas.ts +77 -0
- package/src/observability/forensic-types.ts +10 -0
- package/src/observability/index.ts +21 -27
- package/src/observability/log-reader.ts +161 -111
- package/src/observability/log-writer.ts +41 -83
- package/src/observability/retention.ts +2 -2
- package/src/observability/session-logger.ts +36 -57
- package/src/observability/summary-generator.ts +31 -19
- package/src/observability/types.ts +12 -24
- package/src/orchestrator/contracts/invariants.ts +14 -0
- package/src/orchestrator/contracts/legacy-result-adapter.ts +8 -20
- package/src/orchestrator/error-context.ts +24 -0
- package/src/orchestrator/fallback/event-handler.ts +47 -3
- package/src/orchestrator/handlers/architect.ts +2 -1
- package/src/orchestrator/handlers/build-utils.ts +118 -0
- package/src/orchestrator/handlers/build.ts +42 -219
- package/src/orchestrator/handlers/retrospective.ts +2 -2
- package/src/orchestrator/handlers/types.ts +0 -1
- package/src/orchestrator/lesson-memory.ts +36 -11
- package/src/orchestrator/orchestration-logger.ts +53 -24
- package/src/orchestrator/phase.ts +8 -4
- package/src/orchestrator/progress.ts +63 -0
- package/src/orchestrator/state.ts +79 -17
- package/src/projects/database.ts +47 -0
- package/src/projects/repository.ts +264 -0
- package/src/projects/resolve.ts +301 -0
- package/src/projects/schemas.ts +30 -0
- package/src/projects/types.ts +12 -0
- package/src/review/memory.ts +39 -11
- package/src/review/parse-findings.ts +116 -0
- package/src/review/pipeline.ts +3 -107
- package/src/review/selection.ts +38 -4
- package/src/scoring/time-provider.ts +23 -0
- package/src/tools/doctor.ts +28 -4
- package/src/tools/forensics.ts +7 -12
- package/src/tools/logs.ts +38 -11
- package/src/tools/memory-preferences.ts +157 -0
- package/src/tools/memory-status.ts +17 -96
- package/src/tools/orchestrate.ts +108 -90
- package/src/tools/pipeline-report.ts +3 -2
- package/src/tools/quick.ts +2 -2
- package/src/tools/replay.ts +42 -0
- package/src/tools/review.ts +46 -7
- package/src/tools/session-stats.ts +3 -2
- package/src/tools/summary.ts +43 -0
- package/src/utils/paths.ts +20 -1
- package/src/utils/random.ts +33 -0
- package/src/ux/session-summary.ts +56 -0
package/src/health/checks.ts
CHANGED
|
@@ -4,11 +4,15 @@ import { join } from "node:path";
|
|
|
4
4
|
import type { Config } from "@opencode-ai/plugin";
|
|
5
5
|
import { parse } from "yaml";
|
|
6
6
|
import { loadConfig } from "../config";
|
|
7
|
-
import { DB_FILE, MEMORY_DIR } from "../memory/constants";
|
|
8
7
|
import { AGENT_NAMES } from "../orchestrator/handlers/types";
|
|
9
8
|
import { detectProjectStackTags, filterSkillsByStack } from "../skills/adaptive-injector";
|
|
10
9
|
import { loadAllSkills } from "../skills/loader";
|
|
11
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getAssetsDir,
|
|
12
|
+
getAutopilotDbPath,
|
|
13
|
+
getGlobalConfigDir,
|
|
14
|
+
getLegacyMemoryDbPath,
|
|
15
|
+
} from "../utils/paths";
|
|
12
16
|
import type { HealthResult } from "./types";
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -40,6 +44,103 @@ export async function configHealthCheck(configPath?: string): Promise<HealthResu
|
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
const LATEST_CONFIG_VERSION = 6;
|
|
48
|
+
|
|
49
|
+
export async function configVersionCheck(configPath?: string): Promise<HealthResult> {
|
|
50
|
+
try {
|
|
51
|
+
const config = await loadConfig(configPath);
|
|
52
|
+
if (config === null) {
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
name: "config-version",
|
|
55
|
+
status: "fail" as const,
|
|
56
|
+
message: "Config file not found",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (config.version < LATEST_CONFIG_VERSION) {
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
name: "config-version",
|
|
62
|
+
status: "warn" as const,
|
|
63
|
+
message: `Config v${config.version} is outdated (latest: v${LATEST_CONFIG_VERSION}). Auto-migration will upgrade on next load.`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
name: "config-version",
|
|
68
|
+
status: "pass" as const,
|
|
69
|
+
message: `Config is on latest version (v${config.version})`,
|
|
70
|
+
});
|
|
71
|
+
} catch (error: unknown) {
|
|
72
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
name: "config-version",
|
|
75
|
+
status: "fail" as const,
|
|
76
|
+
message: `Config version check failed: ${msg}`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const REQUIRED_GROUPS: readonly string[] = Object.freeze([
|
|
82
|
+
"architects",
|
|
83
|
+
"challengers",
|
|
84
|
+
"builders",
|
|
85
|
+
"reviewers",
|
|
86
|
+
"red-team",
|
|
87
|
+
"researchers",
|
|
88
|
+
"communicators",
|
|
89
|
+
"utilities",
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
export async function configGroupsCheck(configPath?: string): Promise<HealthResult> {
|
|
93
|
+
try {
|
|
94
|
+
const config = await loadConfig(configPath);
|
|
95
|
+
if (config === null) {
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
name: "config-groups",
|
|
98
|
+
status: "fail" as const,
|
|
99
|
+
message: "Config file not found",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const assignedGroups = Object.keys(config.groups);
|
|
104
|
+
const missingGroups = REQUIRED_GROUPS.filter((g) => !assignedGroups.includes(g));
|
|
105
|
+
|
|
106
|
+
if (missingGroups.length > 0) {
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
name: "config-groups",
|
|
109
|
+
status: "warn" as const,
|
|
110
|
+
message: `Missing model assignments for groups: ${missingGroups.join(", ")}`,
|
|
111
|
+
details: Object.freeze(missingGroups),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const groupsWithoutPrimary = assignedGroups.filter((g) => {
|
|
116
|
+
const group = config.groups[g];
|
|
117
|
+
return !group?.primary;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (groupsWithoutPrimary.length > 0) {
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
name: "config-groups",
|
|
123
|
+
status: "warn" as const,
|
|
124
|
+
message: `Groups without primary model: ${groupsWithoutPrimary.join(", ")}`,
|
|
125
|
+
details: Object.freeze(groupsWithoutPrimary),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
name: "config-groups",
|
|
131
|
+
status: "pass" as const,
|
|
132
|
+
message: `All ${REQUIRED_GROUPS.length} required groups have primary models assigned`,
|
|
133
|
+
});
|
|
134
|
+
} catch (error: unknown) {
|
|
135
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
name: "config-groups",
|
|
138
|
+
status: "fail" as const,
|
|
139
|
+
message: `Config groups check failed: ${msg}`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
43
144
|
/** Standard agent names, derived from the agents barrel export. */
|
|
44
145
|
const STANDARD_AGENT_NAMES: readonly string[] = Object.freeze([
|
|
45
146
|
"researcher",
|
|
@@ -249,19 +350,40 @@ export async function skillHealthCheck(
|
|
|
249
350
|
*/
|
|
250
351
|
export async function memoryHealthCheck(baseDir?: string): Promise<HealthResult> {
|
|
251
352
|
const resolvedBase = baseDir ?? getGlobalConfigDir();
|
|
252
|
-
const dbPath =
|
|
353
|
+
const dbPath = getAutopilotDbPath(resolvedBase);
|
|
354
|
+
const legacyDbPath = getLegacyMemoryDbPath(resolvedBase);
|
|
253
355
|
|
|
254
356
|
try {
|
|
255
357
|
await access(dbPath);
|
|
256
358
|
} catch (error: unknown) {
|
|
257
359
|
const code = (error as NodeJS.ErrnoException).code;
|
|
258
360
|
if (code === "ENOENT") {
|
|
361
|
+
try {
|
|
362
|
+
await access(legacyDbPath);
|
|
363
|
+
} catch (legacyError: unknown) {
|
|
364
|
+
const legacyCode = (legacyError as NodeJS.ErrnoException).code;
|
|
365
|
+
if (legacyCode === "ENOENT") {
|
|
366
|
+
return Object.freeze({
|
|
367
|
+
name: "memory-db",
|
|
368
|
+
status: "pass" as const,
|
|
369
|
+
message: `Memory DB not yet initialized -- will be created on first memory capture`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const legacyMsg = legacyError instanceof Error ? legacyError.message : String(legacyError);
|
|
373
|
+
return Object.freeze({
|
|
374
|
+
name: "memory-db",
|
|
375
|
+
status: "fail" as const,
|
|
376
|
+
message: `Memory DB inaccessible: ${legacyMsg}`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
259
380
|
return Object.freeze({
|
|
260
381
|
name: "memory-db",
|
|
261
382
|
status: "pass" as const,
|
|
262
|
-
message: `
|
|
383
|
+
message: `Legacy memory DB found -- unified DB will be created on next write`,
|
|
263
384
|
});
|
|
264
385
|
}
|
|
386
|
+
|
|
265
387
|
const msg = error instanceof Error ? error.message : String(error);
|
|
266
388
|
return Object.freeze({
|
|
267
389
|
name: "memory-db",
|
package/src/health/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,7 +4,13 @@ import { isFirstLoad, loadConfig } from "./config";
|
|
|
4
4
|
import { runHealthChecks } from "./health/runner";
|
|
5
5
|
import { createAntiSlopHandler } from "./hooks/anti-slop";
|
|
6
6
|
import { installAssets } from "./installer";
|
|
7
|
-
import {
|
|
7
|
+
import { getLogger, initLoggers } from "./logging/domains";
|
|
8
|
+
import {
|
|
9
|
+
createMemoryCaptureHandler,
|
|
10
|
+
createMemoryChatMessageHandler,
|
|
11
|
+
createMemoryInjector,
|
|
12
|
+
getMemoryDb,
|
|
13
|
+
} from "./memory";
|
|
8
14
|
import { ContextMonitor } from "./observability/context-monitor";
|
|
9
15
|
import {
|
|
10
16
|
createObservabilityEventHandler,
|
|
@@ -12,9 +18,9 @@ import {
|
|
|
12
18
|
createToolExecuteBeforeHandler,
|
|
13
19
|
} from "./observability/event-handlers";
|
|
14
20
|
import { SessionEventStore } from "./observability/event-store";
|
|
21
|
+
import { createForensicEvent } from "./observability/forensic-log";
|
|
15
22
|
import { writeSessionLog } from "./observability/log-writer";
|
|
16
23
|
import { pruneOldLogs } from "./observability/retention";
|
|
17
|
-
import type { SessionEvent } from "./observability/types";
|
|
18
24
|
import type { SdkOperations } from "./orchestrator/fallback";
|
|
19
25
|
import {
|
|
20
26
|
createChatMessageHandler,
|
|
@@ -38,6 +44,7 @@ import { ocDoctor, setOpenCodeConfig as setDoctorOpenCodeConfig } from "./tools/
|
|
|
38
44
|
import { ocForensics } from "./tools/forensics";
|
|
39
45
|
import { ocHashlineEdit } from "./tools/hashline-edit";
|
|
40
46
|
import { ocLogs } from "./tools/logs";
|
|
47
|
+
import { ocMemoryPreferences } from "./tools/memory-preferences";
|
|
41
48
|
import { ocMemoryStatus } from "./tools/memory-status";
|
|
42
49
|
import { ocMockFallback } from "./tools/mock-fallback";
|
|
43
50
|
import { ocOrchestrate } from "./tools/orchestrate";
|
|
@@ -49,17 +56,36 @@ import { ocReview } from "./tools/review";
|
|
|
49
56
|
import { ocSessionStats } from "./tools/session-stats";
|
|
50
57
|
import { ocState } from "./tools/state";
|
|
51
58
|
import { ocStocktake } from "./tools/stocktake";
|
|
59
|
+
import { ocSummary } from "./tools/summary";
|
|
52
60
|
import { ocUpdateDocs } from "./tools/update-docs";
|
|
53
61
|
|
|
54
62
|
let openCodeConfig: Config | null = null;
|
|
55
63
|
|
|
64
|
+
let processHandlersRegistered = false;
|
|
65
|
+
function registerProcessHandlers() {
|
|
66
|
+
if (processHandlersRegistered) return;
|
|
67
|
+
processHandlersRegistered = true;
|
|
68
|
+
process.on("uncaughtException", (error) => {
|
|
69
|
+
getLogger("system").error("Uncaught exception", {
|
|
70
|
+
error: error instanceof Error ? error.stack : String(error),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
process.on("unhandledRejection", (reason) => {
|
|
74
|
+
getLogger("system").error("Unhandled rejection", {
|
|
75
|
+
reason: reason instanceof Error ? reason.stack : String(reason),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
56
80
|
const plugin: Plugin = async (input) => {
|
|
57
81
|
const client = input.client;
|
|
82
|
+
initLoggers(process.cwd());
|
|
83
|
+
registerProcessHandlers();
|
|
58
84
|
|
|
59
85
|
// Self-healing asset installation on every load
|
|
60
86
|
const installResult = await installAssets();
|
|
61
87
|
if (installResult.errors.length > 0) {
|
|
62
|
-
|
|
88
|
+
getLogger("system").warn("Asset installation errors", { errors: installResult.errors });
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
// Discover available providers/models in the background (non-blocking).
|
|
@@ -96,7 +122,9 @@ const plugin: Plugin = async (input) => {
|
|
|
96
122
|
|
|
97
123
|
// Retention pruning on load (non-blocking per D-14)
|
|
98
124
|
pruneOldLogs().catch((err) => {
|
|
99
|
-
|
|
125
|
+
getLogger("system").error("Log retention pruning failed", {
|
|
126
|
+
error: err instanceof Error ? err.stack : String(err),
|
|
127
|
+
});
|
|
100
128
|
});
|
|
101
129
|
|
|
102
130
|
// --- Fallback subsystem initialization ---
|
|
@@ -148,6 +176,29 @@ const plugin: Plugin = async (input) => {
|
|
|
148
176
|
manager,
|
|
149
177
|
sdk: sdkOps,
|
|
150
178
|
config: fallbackConfig,
|
|
179
|
+
onFallbackEvent: (event) => {
|
|
180
|
+
if (event.type === "fallback") {
|
|
181
|
+
eventStore.appendEvent(event.sessionId, {
|
|
182
|
+
type: "fallback",
|
|
183
|
+
timestamp: new Date().toISOString(),
|
|
184
|
+
sessionId: event.sessionId,
|
|
185
|
+
failedModel: event.failedModel ?? "unknown",
|
|
186
|
+
nextModel: event.nextModel ?? "unknown",
|
|
187
|
+
reason: event.reason ?? "fallback",
|
|
188
|
+
success: event.success === true,
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
eventStore.appendEvent(event.sessionId, {
|
|
194
|
+
type: "model_switch",
|
|
195
|
+
timestamp: new Date().toISOString(),
|
|
196
|
+
sessionId: event.sessionId,
|
|
197
|
+
fromModel: event.fromModel ?? "unknown",
|
|
198
|
+
toModel: event.toModel ?? "unknown",
|
|
199
|
+
trigger: event.trigger ?? "fallback",
|
|
200
|
+
});
|
|
201
|
+
},
|
|
151
202
|
});
|
|
152
203
|
const chatMessageHandler = createChatMessageHandler(manager);
|
|
153
204
|
const toolExecuteAfterHandler = createToolExecuteAfterHandler(manager);
|
|
@@ -165,6 +216,9 @@ const plugin: Plugin = async (input) => {
|
|
|
165
216
|
const memoryCaptureHandler = memoryConfig.enabled
|
|
166
217
|
? createMemoryCaptureHandler({ getDb: () => getMemoryDb(), projectRoot: process.cwd() })
|
|
167
218
|
: null;
|
|
219
|
+
const memoryChatMessageHandler = memoryConfig.enabled
|
|
220
|
+
? createMemoryChatMessageHandler({ getDb: () => getMemoryDb(), projectRoot: process.cwd() })
|
|
221
|
+
: null;
|
|
168
222
|
|
|
169
223
|
const memoryInjector = memoryConfig.enabled
|
|
170
224
|
? createMemoryInjector({
|
|
@@ -183,18 +237,73 @@ const plugin: Plugin = async (input) => {
|
|
|
183
237
|
showToast: sdkOps.showToast,
|
|
184
238
|
writeSessionLog: async (sessionData) => {
|
|
185
239
|
if (!sessionData) return;
|
|
186
|
-
// Filter to schema-valid event types that match SessionEvent discriminated union
|
|
187
|
-
const schemaEvents: SessionEvent[] = sessionData.events.filter(
|
|
188
|
-
(e): e is SessionEvent =>
|
|
189
|
-
e.type === "fallback" ||
|
|
190
|
-
e.type === "error" ||
|
|
191
|
-
e.type === "decision" ||
|
|
192
|
-
e.type === "model_switch",
|
|
193
|
-
);
|
|
194
240
|
await writeSessionLog({
|
|
241
|
+
projectRoot: process.cwd(),
|
|
195
242
|
sessionId: sessionData.sessionId,
|
|
196
243
|
startedAt: sessionData.startedAt,
|
|
197
|
-
events:
|
|
244
|
+
events: sessionData.events.map((event) =>
|
|
245
|
+
createForensicEvent({
|
|
246
|
+
projectRoot: process.cwd(),
|
|
247
|
+
domain: "session",
|
|
248
|
+
timestamp: event.timestamp,
|
|
249
|
+
sessionId: event.sessionId,
|
|
250
|
+
type: event.type,
|
|
251
|
+
message: event.type === "error" ? event.message : null,
|
|
252
|
+
code:
|
|
253
|
+
event.type === "error"
|
|
254
|
+
? event.errorType
|
|
255
|
+
: event.type === "fallback"
|
|
256
|
+
? "FALLBACK"
|
|
257
|
+
: null,
|
|
258
|
+
payload:
|
|
259
|
+
event.type === "error"
|
|
260
|
+
? {
|
|
261
|
+
model: event.model,
|
|
262
|
+
errorType: event.errorType,
|
|
263
|
+
...(event.statusCode !== undefined ? { statusCode: event.statusCode } : {}),
|
|
264
|
+
}
|
|
265
|
+
: event.type === "fallback"
|
|
266
|
+
? {
|
|
267
|
+
failedModel: event.failedModel,
|
|
268
|
+
nextModel: event.nextModel,
|
|
269
|
+
reason: event.reason,
|
|
270
|
+
success: event.success,
|
|
271
|
+
}
|
|
272
|
+
: event.type === "decision"
|
|
273
|
+
? {
|
|
274
|
+
decision: event.decision,
|
|
275
|
+
rationale: event.rationale,
|
|
276
|
+
}
|
|
277
|
+
: event.type === "model_switch"
|
|
278
|
+
? {
|
|
279
|
+
fromModel: event.fromModel,
|
|
280
|
+
toModel: event.toModel,
|
|
281
|
+
trigger: event.trigger,
|
|
282
|
+
}
|
|
283
|
+
: event.type === "context_warning"
|
|
284
|
+
? {
|
|
285
|
+
utilization: event.utilization,
|
|
286
|
+
contextLimit: event.contextLimit,
|
|
287
|
+
inputTokens: event.inputTokens,
|
|
288
|
+
}
|
|
289
|
+
: event.type === "tool_complete"
|
|
290
|
+
? {
|
|
291
|
+
tool: event.tool,
|
|
292
|
+
durationMs: event.durationMs,
|
|
293
|
+
success: event.success,
|
|
294
|
+
}
|
|
295
|
+
: event.type === "phase_transition"
|
|
296
|
+
? {
|
|
297
|
+
fromPhase: event.fromPhase,
|
|
298
|
+
toPhase: event.toPhase,
|
|
299
|
+
}
|
|
300
|
+
: event.type === "compacted"
|
|
301
|
+
? {
|
|
302
|
+
trigger: event.trigger,
|
|
303
|
+
}
|
|
304
|
+
: {},
|
|
305
|
+
}),
|
|
306
|
+
),
|
|
198
307
|
});
|
|
199
308
|
},
|
|
200
309
|
});
|
|
@@ -220,10 +329,12 @@ const plugin: Plugin = async (input) => {
|
|
|
220
329
|
oc_logs: ocLogs,
|
|
221
330
|
oc_session_stats: ocSessionStats,
|
|
222
331
|
oc_pipeline_report: ocPipelineReport,
|
|
332
|
+
oc_summary: ocSummary,
|
|
223
333
|
oc_mock_fallback: ocMockFallback,
|
|
224
334
|
oc_stocktake: ocStocktake,
|
|
225
335
|
oc_update_docs: ocUpdateDocs,
|
|
226
336
|
oc_memory_status: ocMemoryStatus,
|
|
337
|
+
oc_memory_preferences: ocMemoryPreferences,
|
|
227
338
|
},
|
|
228
339
|
event: async ({ event }) => {
|
|
229
340
|
// 1. Observability: collect (pure observer, no side effects on session)
|
|
@@ -269,6 +380,10 @@ const plugin: Plugin = async (input) => {
|
|
|
269
380
|
parts: unknown[];
|
|
270
381
|
},
|
|
271
382
|
) => {
|
|
383
|
+
if (memoryChatMessageHandler) {
|
|
384
|
+
await memoryChatMessageHandler(hookInput, output);
|
|
385
|
+
}
|
|
386
|
+
|
|
272
387
|
if (fallbackConfig.enabled) {
|
|
273
388
|
await chatMessageHandler(hookInput, output);
|
|
274
389
|
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InspectEventSummary,
|
|
3
|
+
InspectLessonSummary,
|
|
4
|
+
InspectMemoryOverview,
|
|
5
|
+
InspectPreferenceSummary,
|
|
6
|
+
InspectProjectDetails,
|
|
7
|
+
InspectProjectSummary,
|
|
8
|
+
InspectRunSummary,
|
|
9
|
+
} from "./repository";
|
|
10
|
+
|
|
11
|
+
function sanitizeCell(value: string | number | boolean | null): string {
|
|
12
|
+
return String(value ?? "")
|
|
13
|
+
.replace(/\|/g, "\\|")
|
|
14
|
+
.replace(/\n/g, " ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatTimestamp(value: string | null): string {
|
|
18
|
+
return value ?? "-";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatProjects(projects: readonly InspectProjectSummary[]): string {
|
|
22
|
+
if (projects.length === 0) {
|
|
23
|
+
return "No projects found.";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lines = [
|
|
27
|
+
"Projects",
|
|
28
|
+
"",
|
|
29
|
+
"| Project | Current Path | Updated | Runs | Events | Lessons |",
|
|
30
|
+
"|---------|--------------|---------|------|--------|---------|",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const project of projects) {
|
|
34
|
+
lines.push(
|
|
35
|
+
`| ${sanitizeCell(project.name)} | ${sanitizeCell(project.path)} | ${sanitizeCell(project.lastUpdated)} | ${project.runCount} | ${project.eventCount} | ${project.lessonCount} |`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatProjectDetails(details: InspectProjectDetails): string {
|
|
43
|
+
const { project, paths, gitFingerprints } = details;
|
|
44
|
+
const lines = [
|
|
45
|
+
`Project: ${project.name}`,
|
|
46
|
+
"",
|
|
47
|
+
`ID: ${project.id}`,
|
|
48
|
+
`Current Path: ${project.path}`,
|
|
49
|
+
`First Seen: ${project.firstSeenAt}`,
|
|
50
|
+
`Last Updated: ${project.lastUpdated}`,
|
|
51
|
+
`Runs: ${project.runCount}`,
|
|
52
|
+
`Events: ${project.eventCount}`,
|
|
53
|
+
`Lessons: ${project.lessonCount}`,
|
|
54
|
+
`Observations: ${project.observationCount}`,
|
|
55
|
+
"",
|
|
56
|
+
"Paths:",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
if (paths.length === 0) {
|
|
60
|
+
lines.push("- none");
|
|
61
|
+
} else {
|
|
62
|
+
for (const path of paths) {
|
|
63
|
+
lines.push(`- ${path.path}${path.isCurrent ? " [current]" : ""}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
lines.push("", "Git Fingerprints:");
|
|
68
|
+
if (gitFingerprints.length === 0) {
|
|
69
|
+
lines.push("- none");
|
|
70
|
+
} else {
|
|
71
|
+
for (const fingerprint of gitFingerprints) {
|
|
72
|
+
lines.push(
|
|
73
|
+
`- ${fingerprint.normalizedRemoteUrl}${fingerprint.defaultBranch ? ` (${fingerprint.defaultBranch})` : ""}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function formatRuns(runs: readonly InspectRunSummary[]): string {
|
|
82
|
+
if (runs.length === 0) {
|
|
83
|
+
return "No runs found.";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lines = [
|
|
87
|
+
"Runs",
|
|
88
|
+
"",
|
|
89
|
+
"| Project | Run ID | Status | Phase | Revision | Updated |",
|
|
90
|
+
"|---------|--------|--------|-------|----------|---------|",
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
for (const run of runs) {
|
|
94
|
+
lines.push(
|
|
95
|
+
`| ${sanitizeCell(run.projectName)} | ${sanitizeCell(run.runId)} | ${sanitizeCell(run.status)} | ${sanitizeCell(run.currentPhase ?? "-")} | ${run.stateRevision} | ${sanitizeCell(run.lastUpdatedAt)} |`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatEvents(events: readonly InspectEventSummary[]): string {
|
|
103
|
+
if (events.length === 0) {
|
|
104
|
+
return "No events found.";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lines = [
|
|
108
|
+
"Events",
|
|
109
|
+
"",
|
|
110
|
+
"| Timestamp | Project | Domain | Type | Phase | Agent | Code | Message |",
|
|
111
|
+
"|-----------|---------|--------|------|-------|-------|------|---------|",
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (const event of events) {
|
|
115
|
+
lines.push(
|
|
116
|
+
`| ${sanitizeCell(event.timestamp)} | ${sanitizeCell(event.projectName)} | ${sanitizeCell(event.domain)} | ${sanitizeCell(event.type)} | ${sanitizeCell(event.phase ?? "-")} | ${sanitizeCell(event.agent ?? "-")} | ${sanitizeCell(event.code ?? "-")} | ${sanitizeCell(event.message ?? "")} |`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function formatLessons(lessons: readonly InspectLessonSummary[]): string {
|
|
124
|
+
if (lessons.length === 0) {
|
|
125
|
+
return "No lessons found.";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const lines = [
|
|
129
|
+
"Lessons",
|
|
130
|
+
"",
|
|
131
|
+
"| Extracted | Project | Domain | Source Phase | Content |",
|
|
132
|
+
"|-----------|---------|--------|--------------|---------|",
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
for (const lesson of lessons) {
|
|
136
|
+
lines.push(
|
|
137
|
+
`| ${sanitizeCell(lesson.extractedAt)} | ${sanitizeCell(lesson.projectName)} | ${sanitizeCell(lesson.domain)} | ${sanitizeCell(lesson.sourcePhase)} | ${sanitizeCell(lesson.content)} |`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function formatPreferences(preferences: readonly InspectPreferenceSummary[]): string {
|
|
145
|
+
if (preferences.length === 0) {
|
|
146
|
+
return "No preferences found.";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lines = [
|
|
150
|
+
"Preferences",
|
|
151
|
+
"",
|
|
152
|
+
"| Key | Scope | Value | Confidence | Evidence | Updated |",
|
|
153
|
+
"|-----|-------|-------|------------|----------|---------|",
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (const preference of preferences) {
|
|
157
|
+
lines.push(
|
|
158
|
+
`| ${sanitizeCell(preference.key)} | ${sanitizeCell(preference.scope)}${preference.projectId ? `:${sanitizeCell(preference.projectId)}` : ""} | ${sanitizeCell(preference.value)} | ${sanitizeCell(preference.confidence)} | ${sanitizeCell(preference.evidenceCount)} | ${sanitizeCell(preference.lastUpdated)} |`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function formatMemoryOverview(overview: InspectMemoryOverview): string {
|
|
166
|
+
const lines = [
|
|
167
|
+
"Memory Overview",
|
|
168
|
+
"",
|
|
169
|
+
`Total observations: ${overview.stats.totalObservations}`,
|
|
170
|
+
`Total projects: ${overview.stats.totalProjects}`,
|
|
171
|
+
`Total preferences: ${overview.stats.totalPreferences}`,
|
|
172
|
+
`Storage size: ${overview.stats.storageSizeKb} KB`,
|
|
173
|
+
"",
|
|
174
|
+
"Observations by type:",
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for (const [type, count] of Object.entries(overview.stats.observationsByType)) {
|
|
178
|
+
lines.push(`- ${type}: ${count}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
lines.push("", "Recent observations:");
|
|
182
|
+
if (overview.recentObservations.length === 0) {
|
|
183
|
+
lines.push("- none");
|
|
184
|
+
} else {
|
|
185
|
+
for (const observation of overview.recentObservations) {
|
|
186
|
+
lines.push(
|
|
187
|
+
`- [${observation.type}] ${observation.summary} (${formatTimestamp(observation.createdAt)})`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lines.push("", "Preferences:");
|
|
193
|
+
if (overview.preferences.length === 0) {
|
|
194
|
+
lines.push("- none");
|
|
195
|
+
} else {
|
|
196
|
+
for (const preference of overview.preferences) {
|
|
197
|
+
lines.push(
|
|
198
|
+
`- ${preference.key}: ${preference.value} (${preference.scope}, confidence ${preference.confidence}, evidence ${preference.evidenceCount})`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function formatPaths(details: InspectProjectDetails): string {
|
|
207
|
+
if (details.paths.length === 0) {
|
|
208
|
+
return `No paths found for ${details.project.name}.`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const lines = [
|
|
212
|
+
`Paths for ${details.project.name}`,
|
|
213
|
+
"",
|
|
214
|
+
"| Path | Current | First Seen | Last Updated |",
|
|
215
|
+
"|------|---------|------------|--------------|",
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
for (const path of details.paths) {
|
|
219
|
+
lines.push(
|
|
220
|
+
`| ${sanitizeCell(path.path)} | ${path.isCurrent ? "yes" : "no"} | ${sanitizeCell(path.firstSeenAt)} | ${sanitizeCell(path.lastUpdated)} |`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return lines.join("\n");
|
|
225
|
+
}
|