gyoshu 0.1.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.
Files changed (59) hide show
  1. package/AGENTS.md +1039 -0
  2. package/README.ja.md +390 -0
  3. package/README.ko.md +385 -0
  4. package/README.md +459 -0
  5. package/README.zh.md +383 -0
  6. package/bin/gyoshu.js +295 -0
  7. package/install.sh +241 -0
  8. package/package.json +65 -0
  9. package/src/agent/baksa.md +494 -0
  10. package/src/agent/executor.md +1851 -0
  11. package/src/agent/gyoshu.md +2351 -0
  12. package/src/agent/jogyo-feedback.md +137 -0
  13. package/src/agent/jogyo-insight.md +359 -0
  14. package/src/agent/jogyo-paper-writer.md +370 -0
  15. package/src/agent/jogyo.md +1445 -0
  16. package/src/agent/plan-reviewer.md +1862 -0
  17. package/src/agent/plan.md +97 -0
  18. package/src/agent/task-orchestrator.md +1121 -0
  19. package/src/bridge/gyoshu_bridge.py +782 -0
  20. package/src/command/analyze-knowledge.md +840 -0
  21. package/src/command/analyze-plans.md +513 -0
  22. package/src/command/execute.md +893 -0
  23. package/src/command/generate-policy.md +924 -0
  24. package/src/command/generate-suggestions.md +1111 -0
  25. package/src/command/gyoshu-auto.md +258 -0
  26. package/src/command/gyoshu.md +1352 -0
  27. package/src/command/learn.md +1181 -0
  28. package/src/command/planner.md +630 -0
  29. package/src/lib/artifact-security.ts +159 -0
  30. package/src/lib/atomic-write.ts +107 -0
  31. package/src/lib/cell-identity.ts +176 -0
  32. package/src/lib/checkpoint-schema.ts +455 -0
  33. package/src/lib/environment-capture.ts +181 -0
  34. package/src/lib/filesystem-check.ts +84 -0
  35. package/src/lib/literature-client.ts +1048 -0
  36. package/src/lib/marker-parser.ts +474 -0
  37. package/src/lib/notebook-frontmatter.ts +835 -0
  38. package/src/lib/paths.ts +799 -0
  39. package/src/lib/pdf-export.ts +340 -0
  40. package/src/lib/quality-gates.ts +369 -0
  41. package/src/lib/readme-index.ts +462 -0
  42. package/src/lib/report-markdown.ts +870 -0
  43. package/src/lib/session-lock.ts +411 -0
  44. package/src/plugin/gyoshu-hooks.ts +140 -0
  45. package/src/skill/data-analysis/SKILL.md +369 -0
  46. package/src/skill/experiment-design/SKILL.md +374 -0
  47. package/src/skill/ml-rigor/SKILL.md +672 -0
  48. package/src/skill/scientific-method/SKILL.md +331 -0
  49. package/src/tool/checkpoint-manager.ts +1387 -0
  50. package/src/tool/gyoshu-completion.ts +493 -0
  51. package/src/tool/gyoshu-snapshot.ts +745 -0
  52. package/src/tool/literature-search.ts +389 -0
  53. package/src/tool/migration-tool.ts +1404 -0
  54. package/src/tool/notebook-search.ts +794 -0
  55. package/src/tool/notebook-writer.ts +391 -0
  56. package/src/tool/python-repl.ts +1038 -0
  57. package/src/tool/research-manager.ts +1494 -0
  58. package/src/tool/retrospective-store.ts +347 -0
  59. package/src/tool/session-manager.ts +565 -0
@@ -0,0 +1,565 @@
1
+ /**
2
+ * Session Manager - OpenCode tool for managing Gyoshu runtime sessions
3
+ *
4
+ * Provides runtime-only session management with:
5
+ * - Session locking (acquire/release)
6
+ * - Bridge socket paths
7
+ * - Bridge metadata storage (runtime state only)
8
+ *
9
+ * Note: Durable research data is now stored in notebook frontmatter.
10
+ * This tool only manages ephemeral runtime state in OS temp directories
11
+ * (see paths.ts getRuntimeDir() for resolution order).
12
+ *
13
+ * @module session-manager
14
+ */
15
+
16
+ import { tool } from "@opencode-ai/plugin";
17
+ import * as fs from "fs/promises";
18
+ import * as fsSync from "fs";
19
+ import * as path from "path";
20
+ import { durableAtomicWrite, fileExists, readFile } from "../lib/atomic-write";
21
+ import {
22
+ getRuntimeDir,
23
+ getSessionDir,
24
+ ensureDirSync,
25
+ existsSync,
26
+ } from "../lib/paths";
27
+
28
+ // ===== CONSTANTS =====
29
+
30
+ /**
31
+ * Name of the bridge metadata file (runtime state only)
32
+ */
33
+ const BRIDGE_META_FILE = "bridge_meta.json";
34
+
35
+ /**
36
+ * Name of the session lock file
37
+ */
38
+ const SESSION_LOCK_FILE = "session.lock";
39
+
40
+ // ===== INTERFACES =====
41
+
42
+ /**
43
+ * Python environment info for runtime tracking
44
+ */
45
+ interface PythonEnvInfo {
46
+ /** Environment type (venv, uv, poetry, conda, etc.) */
47
+ type: string;
48
+ /** Path to Python interpreter */
49
+ pythonPath: string;
50
+ }
51
+
52
+ /**
53
+ * A single verification round in the adversarial challenge loop.
54
+ * Tracks the outcome of each verification attempt by Baksa (critic agent).
55
+ */
56
+ interface VerificationRound {
57
+ /** Round number (1, 2, 3, ...) */
58
+ round: number;
59
+ /** ISO 8601 timestamp of verification attempt */
60
+ timestamp: string;
61
+ /** Trust score from 0-100 calculated by Baksa (critic agent) */
62
+ trustScore: number;
63
+ /** Outcome of this verification round */
64
+ outcome: "passed" | "failed" | "rework_requested";
65
+ }
66
+
67
+ /**
68
+ * Verification state for adversarial challenge loops.
69
+ * Tracks the current verification round and history of all attempts.
70
+ */
71
+ interface VerificationState {
72
+ /** Current verification round. 0 = not started, 1+ = active rounds */
73
+ currentRound: number;
74
+ /** Maximum allowed verification rounds before escalation (default: 3) */
75
+ maxRounds: number;
76
+ /** History of verification rounds (rounds are 1-indexed) */
77
+ history: VerificationRound[];
78
+ }
79
+
80
+ /**
81
+ * Bridge metadata - lightweight runtime state only.
82
+ * This is NOT durable research data - just ephemeral session state.
83
+ */
84
+ interface BridgeMeta {
85
+ /** Unique session identifier */
86
+ sessionId: string;
87
+ /** ISO 8601 timestamp when bridge was started */
88
+ bridgeStarted: string;
89
+ /** Python environment information */
90
+ pythonEnv: PythonEnvInfo;
91
+ /** Path to the notebook being edited */
92
+ notebookPath: string;
93
+ /** Human-readable report title for display purposes */
94
+ reportTitle?: string;
95
+ /** Adversarial verification state for challenge loops (optional, runtime only) */
96
+ verification?: VerificationState;
97
+ }
98
+
99
+ // ===== RUNTIME INITIALIZATION =====
100
+
101
+ /**
102
+ * Ensures the runtime directory exists.
103
+ * Runtime is now in OS temp directories, no .gitignore needed.
104
+ */
105
+ async function ensureGyoshuRuntime(): Promise<void> {
106
+ const runtimeDir = getRuntimeDir();
107
+ await fs.mkdir(runtimeDir, { recursive: true });
108
+ }
109
+
110
+ /**
111
+ * Synchronous version for initialization in execute()
112
+ */
113
+ function ensureGyoshuRuntimeSync(): void {
114
+ const runtimeDir = getRuntimeDir();
115
+ ensureDirSync(runtimeDir);
116
+ }
117
+
118
+ // ===== PATH HELPERS =====
119
+
120
+ /**
121
+ * Gets the path to a session's bridge metadata file.
122
+ *
123
+ * @param sessionId - The session identifier
124
+ * @returns Full path to bridge_meta.json
125
+ */
126
+ function getBridgeMetaPath(sessionId: string): string {
127
+ return path.join(getSessionDir(sessionId), BRIDGE_META_FILE);
128
+ }
129
+
130
+ /**
131
+ * Gets the path to a session's lock file.
132
+ *
133
+ * @param sessionId - The session identifier
134
+ * @returns Full path to session.lock
135
+ */
136
+ function getSessionLockFilePath(sessionId: string): string {
137
+ return path.join(getSessionDir(sessionId), SESSION_LOCK_FILE);
138
+ }
139
+
140
+ // ===== VALIDATION =====
141
+
142
+ /** Maximum number of verification history entries to keep */
143
+ const MAX_VERIFICATION_HISTORY = 10;
144
+
145
+ /** Valid outcomes for verification rounds */
146
+ const VALID_OUTCOMES = ["passed", "failed", "rework_requested"] as const;
147
+
148
+ /**
149
+ * Validates a VerificationRound object.
150
+ *
151
+ * @param round - The verification round to validate
152
+ * @param index - Index in history array (for error messages)
153
+ * @returns Error message if invalid, null if valid
154
+ */
155
+ function validateVerificationRound(
156
+ round: unknown,
157
+ index: number
158
+ ): string | null {
159
+ if (!round || typeof round !== "object") {
160
+ return `history[${index}] is not an object`;
161
+ }
162
+
163
+ const r = round as Record<string, unknown>;
164
+
165
+ // Validate round number
166
+ if (typeof r.round !== "number" || !Number.isInteger(r.round) || r.round < 1) {
167
+ return `history[${index}].round must be a positive integer`;
168
+ }
169
+
170
+ // Validate timestamp
171
+ if (typeof r.timestamp !== "string" || r.timestamp.trim() === "") {
172
+ return `history[${index}].timestamp must be a non-empty string`;
173
+ }
174
+
175
+ // Validate trustScore (0-100)
176
+ if (
177
+ typeof r.trustScore !== "number" ||
178
+ r.trustScore < 0 ||
179
+ r.trustScore > 100
180
+ ) {
181
+ return `history[${index}].trustScore must be a number between 0 and 100`;
182
+ }
183
+
184
+ // Validate outcome
185
+ if (!VALID_OUTCOMES.includes(r.outcome as typeof VALID_OUTCOMES[number])) {
186
+ return `history[${index}].outcome must be one of: ${VALID_OUTCOMES.join(", ")}`;
187
+ }
188
+
189
+ return null;
190
+ }
191
+
192
+ /**
193
+ * Validates a VerificationState object.
194
+ *
195
+ * @param state - The verification state to validate
196
+ * @returns Error message if invalid, null if valid
197
+ */
198
+ function validateVerificationState(state: unknown): string | null {
199
+ if (!state || typeof state !== "object") {
200
+ return "verification must be an object";
201
+ }
202
+
203
+ const s = state as Record<string, unknown>;
204
+
205
+ // Validate currentRound (non-negative integer)
206
+ if (
207
+ typeof s.currentRound !== "number" ||
208
+ !Number.isInteger(s.currentRound) ||
209
+ s.currentRound < 0
210
+ ) {
211
+ return "currentRound must be a non-negative integer";
212
+ }
213
+
214
+ // Validate maxRounds (positive integer >= 1)
215
+ if (
216
+ typeof s.maxRounds !== "number" ||
217
+ !Number.isInteger(s.maxRounds) ||
218
+ s.maxRounds < 1
219
+ ) {
220
+ return "maxRounds must be a positive integer >= 1";
221
+ }
222
+
223
+ // Validate currentRound <= maxRounds
224
+ if (s.currentRound > s.maxRounds) {
225
+ return `currentRound (${s.currentRound}) cannot exceed maxRounds (${s.maxRounds})`;
226
+ }
227
+
228
+ // Validate history is an array
229
+ if (!Array.isArray(s.history)) {
230
+ return "history must be an array";
231
+ }
232
+
233
+ // Validate each history entry
234
+ for (let i = 0; i < s.history.length; i++) {
235
+ const error = validateVerificationRound(s.history[i], i);
236
+ if (error) {
237
+ return error;
238
+ }
239
+ }
240
+
241
+ return null;
242
+ }
243
+
244
+ /**
245
+ * Validates that a session ID is safe to use in file paths.
246
+ * Prevents directory traversal and other path injection attacks.
247
+ *
248
+ * @param sessionId - The session ID to validate
249
+ * @throws Error if session ID is invalid
250
+ */
251
+ function validateSessionId(sessionId: string): void {
252
+ if (!sessionId || typeof sessionId !== "string") {
253
+ throw new Error("researchSessionID is required and must be a string");
254
+ }
255
+
256
+ if (sessionId.includes("..") || sessionId.includes("/") || sessionId.includes("\\")) {
257
+ throw new Error("Invalid researchSessionID: contains path traversal characters");
258
+ }
259
+
260
+ if (sessionId.trim().length === 0) {
261
+ throw new Error("Invalid researchSessionID: cannot be empty or whitespace");
262
+ }
263
+
264
+ if (sessionId.length > 255) {
265
+ throw new Error("Invalid researchSessionID: exceeds maximum length of 255 characters");
266
+ }
267
+ }
268
+
269
+ // ===== DEFAULT BRIDGE META =====
270
+
271
+ /**
272
+ * Creates a new bridge metadata object with default values.
273
+ *
274
+ * @param sessionId - The session identifier
275
+ * @param data - Optional initial data to merge
276
+ * @returns A new BridgeMeta object
277
+ */
278
+ function createDefaultBridgeMeta(
279
+ sessionId: string,
280
+ data?: Partial<BridgeMeta>
281
+ ): BridgeMeta {
282
+ const now = new Date().toISOString();
283
+
284
+ return {
285
+ sessionId,
286
+ bridgeStarted: now,
287
+ pythonEnv: {
288
+ type: "unknown",
289
+ pythonPath: "",
290
+ },
291
+ notebookPath: "",
292
+ reportTitle: "",
293
+ ...data,
294
+ };
295
+ }
296
+
297
+ // ===== TOOL DEFINITION =====
298
+
299
+ export default tool({
300
+ description:
301
+ "Manage Gyoshu runtime sessions - create, read, update, delete session state. " +
302
+ "Sessions track bridge metadata, notebook paths, and runtime state. " +
303
+ "Research data is now stored in notebook frontmatter (not session manifests).",
304
+ args: {
305
+ action: tool.schema
306
+ .enum(["create", "get", "list", "update", "delete"])
307
+ .describe("Operation to perform on runtime sessions"),
308
+ researchSessionID: tool.schema
309
+ .string()
310
+ .optional()
311
+ .describe("Unique session identifier (required for create/get/update/delete)"),
312
+ data: tool.schema
313
+ .any()
314
+ .optional()
315
+ .describe(
316
+ "Bridge metadata for create/update operations. Can include: " +
317
+ "pythonEnv (type, pythonPath), notebookPath, reportTitle, " +
318
+ "verification (currentRound, maxRounds, history)"
319
+ ),
320
+ },
321
+
322
+ async execute(args) {
323
+ ensureGyoshuRuntimeSync();
324
+
325
+ switch (args.action) {
326
+ // ===== CREATE =====
327
+ case "create": {
328
+ if (!args.researchSessionID) {
329
+ throw new Error("researchSessionID is required for create action");
330
+ }
331
+ validateSessionId(args.researchSessionID);
332
+
333
+ const sessionDir = getSessionDir(args.researchSessionID);
334
+ const bridgeMetaPath = getBridgeMetaPath(args.researchSessionID);
335
+
336
+ if (await fileExists(bridgeMetaPath)) {
337
+ throw new Error(
338
+ `Session '${args.researchSessionID}' already exists. Use 'update' to modify existing sessions.`
339
+ );
340
+ }
341
+
342
+ await fs.mkdir(sessionDir, { recursive: true });
343
+
344
+ const bridgeMeta = createDefaultBridgeMeta(
345
+ args.researchSessionID,
346
+ args.data as Partial<BridgeMeta>
347
+ );
348
+
349
+ await durableAtomicWrite(bridgeMetaPath, JSON.stringify(bridgeMeta, null, 2));
350
+
351
+ return JSON.stringify(
352
+ {
353
+ success: true,
354
+ action: "create",
355
+ researchSessionID: args.researchSessionID,
356
+ bridgeMeta,
357
+ sessionDir,
358
+ },
359
+ null,
360
+ 2
361
+ );
362
+ }
363
+
364
+ // ===== GET =====
365
+ case "get": {
366
+ if (!args.researchSessionID) {
367
+ throw new Error("researchSessionID is required for get action");
368
+ }
369
+ validateSessionId(args.researchSessionID);
370
+
371
+ const bridgeMetaPath = getBridgeMetaPath(args.researchSessionID);
372
+ const sessionDir = getSessionDir(args.researchSessionID);
373
+ const lockPath = getSessionLockFilePath(args.researchSessionID);
374
+
375
+ if (!(await fileExists(bridgeMetaPath))) {
376
+ throw new Error(`Session '${args.researchSessionID}' not found`);
377
+ }
378
+
379
+ const bridgeMeta = await readFile<BridgeMeta>(bridgeMetaPath, true);
380
+ const isLocked = await fileExists(lockPath);
381
+
382
+ return JSON.stringify(
383
+ {
384
+ success: true,
385
+ action: "get",
386
+ researchSessionID: args.researchSessionID,
387
+ bridgeMeta,
388
+ sessionDir,
389
+ isLocked,
390
+ },
391
+ null,
392
+ 2
393
+ );
394
+ }
395
+
396
+ // ===== LIST =====
397
+ case "list": {
398
+ const sessions: Array<{
399
+ researchSessionID: string;
400
+ bridgeStarted: string;
401
+ notebookPath: string;
402
+ reportTitle: string;
403
+ isLocked: boolean;
404
+ }> = [];
405
+
406
+ const runtimeDir = getRuntimeDir();
407
+
408
+ let entries: Array<{ name: string; isDirectory: () => boolean }>;
409
+ try {
410
+ entries = await fs.readdir(runtimeDir, { withFileTypes: true });
411
+ } catch (err: any) {
412
+ if (err.code === "ENOENT") {
413
+ return JSON.stringify(
414
+ {
415
+ success: true,
416
+ action: "list",
417
+ sessions: [],
418
+ count: 0,
419
+ },
420
+ null,
421
+ 2
422
+ );
423
+ }
424
+ throw err;
425
+ }
426
+
427
+ for (const entry of entries) {
428
+ if (!entry.isDirectory()) continue;
429
+
430
+ const bridgeMetaPath = path.join(runtimeDir, entry.name, BRIDGE_META_FILE);
431
+ const lockPath = path.join(runtimeDir, entry.name, SESSION_LOCK_FILE);
432
+
433
+ try {
434
+ const bridgeMeta = await readFile<BridgeMeta>(bridgeMetaPath, true);
435
+ const isLocked = existsSync(lockPath);
436
+
437
+ sessions.push({
438
+ researchSessionID: bridgeMeta.sessionId,
439
+ bridgeStarted: bridgeMeta.bridgeStarted,
440
+ notebookPath: bridgeMeta.notebookPath,
441
+ reportTitle: bridgeMeta.reportTitle || "",
442
+ isLocked,
443
+ });
444
+ } catch (error) {
445
+ // Log error but continue listing other sessions
446
+ console.error(`[session-manager] Failed to read session ${entry.name}:`, error);
447
+ }
448
+ }
449
+
450
+ sessions.sort(
451
+ (a, b) =>
452
+ new Date(b.bridgeStarted).getTime() - new Date(a.bridgeStarted).getTime()
453
+ );
454
+
455
+ return JSON.stringify(
456
+ {
457
+ success: true,
458
+ action: "list",
459
+ sessions,
460
+ count: sessions.length,
461
+ },
462
+ null,
463
+ 2
464
+ );
465
+ }
466
+
467
+ // ===== UPDATE =====
468
+ case "update": {
469
+ if (!args.researchSessionID) {
470
+ throw new Error("researchSessionID is required for update action");
471
+ }
472
+ validateSessionId(args.researchSessionID);
473
+
474
+ const bridgeMetaPath = getBridgeMetaPath(args.researchSessionID);
475
+
476
+ if (!(await fileExists(bridgeMetaPath))) {
477
+ throw new Error(
478
+ `Session '${args.researchSessionID}' not found. Use 'create' first.`
479
+ );
480
+ }
481
+
482
+ const existing = await readFile<BridgeMeta>(bridgeMetaPath, true);
483
+ const updateData = args.data as Partial<BridgeMeta> | undefined;
484
+
485
+ let sanitizedVerification: VerificationState | undefined = undefined;
486
+ if (updateData?.verification !== undefined) {
487
+ const validationError = validateVerificationState(updateData.verification);
488
+ if (validationError) {
489
+ throw new Error(`Invalid verification state: ${validationError}`);
490
+ }
491
+ const verif = updateData.verification as VerificationState;
492
+ sanitizedVerification = {
493
+ ...verif,
494
+ history: verif.history.slice(-MAX_VERIFICATION_HISTORY),
495
+ };
496
+ }
497
+
498
+ const updated: BridgeMeta = {
499
+ ...existing,
500
+ ...(updateData?.notebookPath !== undefined && {
501
+ notebookPath: updateData.notebookPath,
502
+ }),
503
+ ...(updateData?.reportTitle !== undefined && {
504
+ reportTitle: updateData.reportTitle,
505
+ }),
506
+ ...(sanitizedVerification !== undefined && {
507
+ verification: sanitizedVerification,
508
+ }),
509
+ sessionId: existing.sessionId,
510
+ bridgeStarted: existing.bridgeStarted,
511
+ };
512
+
513
+ if (updateData?.pythonEnv) {
514
+ updated.pythonEnv = {
515
+ ...existing.pythonEnv,
516
+ ...updateData.pythonEnv,
517
+ };
518
+ }
519
+
520
+ await durableAtomicWrite(bridgeMetaPath, JSON.stringify(updated, null, 2));
521
+
522
+ return JSON.stringify(
523
+ {
524
+ success: true,
525
+ action: "update",
526
+ researchSessionID: args.researchSessionID,
527
+ bridgeMeta: updated,
528
+ },
529
+ null,
530
+ 2
531
+ );
532
+ }
533
+
534
+ // ===== DELETE =====
535
+ case "delete": {
536
+ if (!args.researchSessionID) {
537
+ throw new Error("researchSessionID is required for delete action");
538
+ }
539
+ validateSessionId(args.researchSessionID);
540
+
541
+ const sessionDir = getSessionDir(args.researchSessionID);
542
+
543
+ if (!(await fileExists(sessionDir))) {
544
+ throw new Error(`Session '${args.researchSessionID}' not found`);
545
+ }
546
+
547
+ await fs.rm(sessionDir, { recursive: true, force: true });
548
+
549
+ return JSON.stringify(
550
+ {
551
+ success: true,
552
+ action: "delete",
553
+ researchSessionID: args.researchSessionID,
554
+ message: `Session '${args.researchSessionID}' and all runtime data deleted`,
555
+ },
556
+ null,
557
+ 2
558
+ );
559
+ }
560
+
561
+ default:
562
+ throw new Error(`Unknown action: ${args.action}`);
563
+ }
564
+ },
565
+ });