Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name": "mutils",
"source": "./plugins/mutils",
"description": "汎用ユーティリティ(フック・スキル)",
"version": "0.12.1"
"version": "0.13.0"
},
{
"name": "context",
Expand Down
20 changes: 10 additions & 10 deletions .claude/rules/ai-generated/project-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
## Rules

- **全プラグインは mutils がインストールされていることを前提とする** — 各プラグイン・スキルに mutils の詳細説明を書かない
- **セッション管理の説明は `mutils:session-id スキルを使用` とだけ書く** — session-id フォーマットをインラインで重複定義しない
- **session-id の feature-name は kebab-case のみ許可** — 正規表現: `/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/`
- **セッションディレクトリは `.agents/sessions/[session-id]/` 形式** — `.agents/sessions/` だけでも `sessions/yyyymmdd.../` でもない
- **ワークスペース管理の説明は `mutils:workspace-id スキルを使用` とだけ書く** — workspace-id フォーマットをインラインで重複定義しない
- **workspace-id の feature-name は kebab-case のみ許可** — 正規表現: `/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/`
- **ワークスペースディレクトリは `.agents/workspaces/[workspace-id]/` 形式** — `.agents/workspaces/` だけでも `workspaces/yyyymmdd.../` でもない

## Pickup Topics

Expand All @@ -23,29 +23,29 @@
```markdown
# My Plugin Skill

セッション管理には mutils:session-id スキルを使用。
ワークスペース管理には mutils:workspace-id スキルを使用。
```

### Bad Example

```markdown
# My Plugin Skill

セッションIDのフォーマット: yyyymmdd-HHmm-[feature-name]
ディレクトリ構造: .agents/sessions/[session-id]/
ワークスペースIDのフォーマット: yyyymmdd-HHmm-[feature-name]
ディレクトリ構造: .agents/workspaces/[workspace-id]/
ファイル命名: [nnnnnn]-[subagent-name]-[content].md
```

### Session ID フォーマット仕様
### Workspace ID フォーマット仕様

`mutils:session-id` スキルで一元定義されている canonical な仕様:
`mutils:workspace-id` スキルで一元定義されている canonical な仕様:

| 要素 | 仕様 |
|------|------|
| フォーマット | `yyyymmdd-HHmm-[feature-name]` |
| 日時 | ローカル時刻(例: `20260301-1430`) |
| feature-name | kebab-case、正規表現 `/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/` |
| ディレクトリ | `.agents/sessions/[session-id]/` |
| ディレクトリ | `.agents/workspaces/[workspace-id]/` |
| ファイル命名 | `[nnnnnn]-[subagent-name]-[content].md`(6桁ゼロ埋め連番) |

`rounds/` 構造(`rounds/[NNNN]/`)は plan プラグイン固有であり、session-id の一般仕様には含まない。
`rounds/` 構造(`rounds/[NNNN]/`)は plan プラグイン固有であり、workspace-id の一般仕様には含まない。
4 changes: 4 additions & 0 deletions plugins/mutils/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/check-context-version.ts"
},
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/workspace-id-persist.ts"
}
]
}
Expand Down
123 changes: 123 additions & 0 deletions plugins/mutils/hooks/workspace-id-persist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env bun
import { existsSync } from "node:fs";
import path from "node:path";
import {
findGitRoot,
HookLogger,
MutilsDB,
wrapRun,
} from "@r_masseater/cc-plugin-lib";
import { defineHook, runHook } from "cc-hooks-ts";

using logger = HookLogger.fromFile(import.meta.filename);

type WorkspaceMapping = {
id?: number;
workspace_id: string;
feature_name: string;
created_at: string;
};

const WORKSPACE_MAPPINGS_SCHEMA = {
columns: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
workspace_id TEXT NOT NULL UNIQUE,
feature_name TEXT NOT NULL,
created_at TEXT NOT NULL
`,
indexes: [
"CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_mappings_workspace_id ON workspace_mappings(workspace_id)",
],
} as const;

/**
* Get the latest workspace mapping from DB
*/
function getLatestWorkspaceMapping(cwd: string): WorkspaceMapping | null {
try {
using db = MutilsDB.open(cwd);
const table = db.table<WorkspaceMapping>(
"workspace_mappings",
WORKSPACE_MAPPINGS_SCHEMA,
);
// Get the most recent entry
const all = table.findAll("1=1 ORDER BY id DESC LIMIT 1", []);
return all[0] ?? null;
} catch (error) {
logger.error(`Failed to get workspace mapping: ${error}`);
return null;
}
}

const hook = defineHook({
trigger: {
SessionStart: true,
},
run: wrapRun(logger, (context) => {
const cwd = process.cwd();
const eventType = context.input.hook_event_name;

// Handle SessionStart - restore workspace-id after compact or resume
if (eventType === "SessionStart") {
// Check git repository and .agents directory
const gitRoot = findGitRoot(cwd);
if (gitRoot === null) {
logger.debug("Skipping: not a git repository");
return context.success({});
}

const agentsDir = path.join(gitRoot, ".agents");
if (!existsSync(agentsDir)) {
logger.debug("Skipping: .agents directory does not exist");
return context.success({});
}

const source =
"source" in context.input && typeof context.input.source === "string"
? context.input.source
: "startup";

logger.debug(`SessionStart source: ${source}`);

// Only restore on compact or resume
if (source !== "compact" && source !== "resume") {
logger.debug("Not a compact/resume session, skipping");
return context.success({});
}

const mapping = getLatestWorkspaceMapping(gitRoot);

if (!mapping) {
logger.debug("No workspace mapping found");
return context.success({});
}

logger.info(`Restored workspace-id: ${mapping.workspace_id}`);

const sourceDescription =
source === "compact" ? "Auto Compact" : "session resume";

// Provide restored workspace-id to Claude
return context.json({
event: "SessionStart",
output: {
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: `Workspace ID Restored: ${mapping.workspace_id}

The previous workspace-id has been restored after ${sourceDescription}. Continue using this workspace-id for your work.

Workspace directory: .agents/workspaces/${mapping.workspace_id}/`,
},
suppressOutput: true,
},
});
}

return context.success({});
}),
});

if (import.meta.main) {
await runHook(hook);
}
2 changes: 1 addition & 1 deletion plugins/mutils/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mutils",
"description": "汎用ユーティリティ(フック・スキル)",
"version": "0.12.1",
"version": "0.13.0",
"author": {
"name": "masseater"
},
Expand Down
73 changes: 73 additions & 0 deletions plugins/mutils/skills/ccs-handoff/ccs-handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { parseWorkspaceId } from "../workspace-id/patterns.js";

// --- Constants ---

Expand Down Expand Up @@ -446,6 +447,51 @@ function detectSubagents(
return fs.existsSync(subagentDir);
}

// --- Session ID extraction from .agents/sessions/ ---

interface UserSessionId {
fullId: string;
featureName: string;
}
Comment on lines +450 to +455
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Complete the session→workspace rename in handoff output and instructions.

Line 580/583/588/595 and Line 592 still refer to session-id and .agents/sessions/..., which conflicts with the new workspace model and points users to the wrong directory.

🔧 Suggested fix
-// --- Session ID extraction from .agents/sessions/ ---
+// --- Workspace ID extraction from .agents/workspaces/ ---
@@
-	// User Session ID (for handoff)
-	lines.push("## User Session ID");
+	// Workspace ID (for handoff)
+	lines.push("## Workspace ID");
@@
-		lines.push(`**Session ID**: \`${userSessionId.fullId}\``);
+		lines.push(`**Workspace ID**: \`${userSessionId.fullId}\``);
@@
-		lines.push(
-			"To continue using this session-id, create the session directory:",
-		);
+		lines.push(
+			"To continue using this workspace-id, create the workspace directory:",
+		);
@@
-		lines.push(`mkdir -p .agents/sessions/${userSessionId.fullId}`);
+		lines.push(`mkdir -p .agents/workspaces/${userSessionId.fullId}`);
@@
-		lines.push("No user session-id found in .agents/sessions/");
+		lines.push("No workspace-id found in .agents/workspaces/");

Also applies to: 579-596

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/skills/ccs-handoff/ccs-handoff.ts` around lines 450 - 455, The
handoff output and instruction text still reference "session-id" and the
.agents/sessions/... path; update all occurrences to the workspace model (e.g.,
"workspace-id" and .agents/workspaces/...) and adjust types/identifiers
accordingly—rename the UserSessionId concept to UserWorkspaceId (or equivalent)
and update any usages of fullId/featureName that construct or display the path
so the instructions point to .agents/workspaces/<workspace-id> instead of
.agents/sessions/<session-id>; ensure the handoff messages generated in
ccs-handoff.ts (the functions/variables that build the handoff output around
lines ~579-596) are changed to use workspace naming consistently.


function extractUserSessionId(projectPath: string): UserSessionId | null {
const workspacesDir = path.join(projectPath, ".agents", "workspaces");
if (!fs.existsSync(workspacesDir)) {
return null;
}

// Get the most recently modified workspace directory
const entries = fs
.readdirSync(workspacesDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => {
const fullPath = path.join(workspacesDir, d.name);
const stat = fs.statSync(fullPath);
return { name: d.name, mtime: stat.mtimeMs };
})
.sort((a, b) => b.mtime - a.mtime);

if (entries.length === 0) {
return null;
}

const workspaceId = entries[0]?.name;
if (!workspaceId) {
return null;
}

// Validate workspace-id format using shared pattern
const parsed = parseWorkspaceId(workspaceId);
if (!parsed) {
return null;
}

return {
fullId: workspaceId,
featureName: parsed.featureName,
};
}
Comment on lines +457 to +493
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid using “latest modified workspace directory” as the session mapping source.

Selecting entries[0] by mtime can associate the wrong workspace-id when multiple workspaces exist for the same project path. For handoff accuracy, resolve workspace-id by the current Claude session identifier first (the mapping persisted by plugins/mutils/hooks/workspace-id-persist.ts), and only use mtime as an explicit fallback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/skills/ccs-handoff/ccs-handoff.ts` around lines 457 - 493, The
extractUserSessionId function currently picks the most-recent workspace by mtime
which can be incorrect; update extractUserSessionId to first attempt to resolve
the workspace id from the persisted Claude session mapping (the mapping saved by
plugins/mutils/hooks/workspace-id-persist.ts), and only if that lookup returns
null/undefined fall back to the existing mtime-based selection and
parseWorkspaceId validation; keep the existing parseWorkspaceId usage and return
shape (fullId, featureName) unchanged and ensure any IO/lookup errors are
handled the same way as the current existence checks.


// --- Plan file extraction ---

function isPlanFileToolUse(
Expand Down Expand Up @@ -495,6 +541,7 @@ function formatMarkdown(
todos: PendingTodo[],
hasSubagents: boolean,
planFiles: string[],
userSessionId: UserSessionId | null,
): string {
const lines: string[] = [];

Expand Down Expand Up @@ -529,6 +576,26 @@ function formatMarkdown(
lines.push(summaryText);
lines.push("");

// User Session ID (for handoff)
lines.push("## User Session ID");
lines.push("");
if (userSessionId) {
lines.push(`**Session ID**: \`${userSessionId.fullId}\``);
lines.push("");
lines.push(`**Feature Name**: ${userSessionId.featureName}`);
lines.push("");
lines.push(
"To continue using this session-id, create the session directory:",
);
lines.push("");
lines.push("```bash");
lines.push(`mkdir -p .agents/sessions/${userSessionId.fullId}`);
lines.push("```");
} else {
lines.push("No user session-id found in .agents/sessions/");
}
lines.push("");

// First Prompt
lines.push("## First Prompt");
lines.push("");
Expand Down Expand Up @@ -617,6 +684,11 @@ async function main(): Promise<void> {
);
const planFiles = extractPlanFilePaths(sessionInfo.jsonlPath);

// Extract user session-id for handoff
const userSessionId = sessionInfo.projectPath
? extractUserSessionId(sessionInfo.projectPath)
: null;

const markdown = formatMarkdown(
sessionInfo,
messages,
Expand All @@ -625,6 +697,7 @@ async function main(): Promise<void> {
todos,
hasSubagents,
planFiles,
userSessionId,
);

console.log(markdown);
Expand Down
22 changes: 0 additions & 22 deletions plugins/mutils/skills/session-id/generate.ts

This file was deleted.

Loading