mirror of
https://github.com/Yeachan-Heo/oh-my-claudecode.git
synced 2026-10-18 17:29:49 -05:00
fix(session-end): plan chain enqueue on the plugin-path SessionEnd bootstrap (#4170)
* fix(session-end): plan chain enqueue in the plugin-path SessionEnd bootstrap * docs(setup): document marketplace plugin-cache staleness limitation for Flow B * chore(inventory): regenerate after dev merge --------- Co-authored-by: gaebal-gajae <clawdbot@users.noreply.github.com>
This commit is contained in:
1 parent
1232152193
commit
3a47e3898a
5 files changed
+174
-17
No files matched your search
@@ -170,6 +170,14 @@ claude plugin update oh-my-claudecode@oh-my-claudecode
|
||||
/setup
|
||||
```
|
||||
|
||||
**Known limitation**: the plugin cache copy runs at the version you last
|
||||
updated through the marketplace — hooks (including the SessionEnd chain
|
||||
enqueuer) execute the cached code, not your checkout. If session-end behavior
|
||||
looks stale after rebuilding, re-run `claude plugin update` first. OMC already
|
||||
mitigates dangling references to older cache versions at setup time via
|
||||
`scripts/repair-plugin-cache.mjs` (registry rewrite + symlink to the latest
|
||||
valid cache root), but it does not re-copy content into the cache.
|
||||
|
||||
### Flow C: `omc setup --no-plugin` (fallback, bundled skills)
|
||||
|
||||
**Advantages**: Forces local bundled skills to `~/.claude/skills/`, no plugin system.
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
"provenance": {
|
||||
"base": "05c800f40d1ad53b42a78609d2667ef4f726808b",
|
||||
"planningHead": "0a91273e61dbbd47eb0af4c02844409251e08398",
|
||||
"head": "b5f923fc326c26344b8288dbc0b2c688cb33c9f1",
|
||||
"sourceSha256": "3e1826809a13a4a0efbfa96c70b0cce9a29505656d943e0c5838825bda2990cf",
|
||||
"head": "5ce58e58497298b6e99a23c70f311204c40bde5a",
|
||||
"sourceSha256": "61520b8ca140e3673bc6815fb4b32c0f296e0e7898dc6efb7439484c431f1f9c",
|
||||
"generatedAt": null,
|
||||
"generator": "scripts/generate-inventory-graph.mjs"
|
||||
},
|
||||
"base": "05c800f40d1ad53b42a78609d2667ef4f726808b",
|
||||
"planningHead": "0a91273e61dbbd47eb0af4c02844409251e08398",
|
||||
"head": "b5f923fc326c26344b8288dbc0b2c688cb33c9f1",
|
||||
"sourceSha256": "3e1826809a13a4a0efbfa96c70b0cce9a29505656d943e0c5838825bda2990cf",
|
||||
"head": "5ce58e58497298b6e99a23c70f311204c40bde5a",
|
||||
"sourceSha256": "61520b8ca140e3673bc6815fb4b32c0f296e0e7898dc6efb7439484c431f1f9c",
|
||||
"counts": {
|
||||
"public": {
|
||||
"skills": 47,
|
||||
@@ -23,13 +23,13 @@
|
||||
"total": 96
|
||||
},
|
||||
"internal": {
|
||||
"hookFiles": 329,
|
||||
"hookFiles": 330,
|
||||
"featureFiles": 84,
|
||||
"toolFiles": 61,
|
||||
"libFiles": 47,
|
||||
"templateHooks": 23,
|
||||
"srcFiles": 1414,
|
||||
"total": 1437
|
||||
"srcFiles": 1415,
|
||||
"total": 1438
|
||||
},
|
||||
"generated": {
|
||||
"distFiles": 5066,
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"skills": 47,
|
||||
"commands": 21,
|
||||
"hookFiles": 329,
|
||||
"hookFiles": 330,
|
||||
"workflows": 8,
|
||||
"agentDefinitions": 18,
|
||||
"promptLikeFiles": 61
|
||||
@@ -398,6 +398,7 @@
|
||||
"src/hooks/session-end/__tests__/chain-enqueuer.test.ts",
|
||||
"src/hooks/session-end/__tests__/cleanup-manifest.test.ts",
|
||||
"src/hooks/session-end/__tests__/duplicate-notifications.test.ts",
|
||||
"src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"src/hooks/session-end/__tests__/guardrails.test.ts",
|
||||
"src/hooks/session-end/__tests__/mode-state-cleanup.test.ts",
|
||||
"src/hooks/session-end/__tests__/openclaw-session-end.test.ts",
|
||||
@@ -6219,6 +6220,7 @@
|
||||
"src/hooks/session-end/__tests__/chain-enqueuer.test.ts",
|
||||
"src/hooks/session-end/__tests__/cleanup-manifest.test.ts",
|
||||
"src/hooks/session-end/__tests__/duplicate-notifications.test.ts",
|
||||
"src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"src/hooks/session-end/__tests__/guardrails.test.ts",
|
||||
"src/hooks/session-end/__tests__/mode-state-cleanup.test.ts",
|
||||
"src/hooks/session-end/__tests__/openclaw-session-end.test.ts",
|
||||
@@ -38620,6 +38622,11 @@
|
||||
"kind": "hook",
|
||||
"path": "src/hooks/session-end/__tests__/duplicate-notifications.test.ts"
|
||||
},
|
||||
{
|
||||
"id": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"kind": "hook",
|
||||
"path": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts"
|
||||
},
|
||||
{
|
||||
"id": "src/hooks/session-end/__tests__/guardrails.test.ts",
|
||||
"kind": "hook",
|
||||
@@ -64032,6 +64039,51 @@
|
||||
"to": "src/notifications/index.ts",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"to": "external:child_process",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"to": "external:fs",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"to": "external:os",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"to": "external:path",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"to": "external:vitest",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"to": "src/hooks/session-end/chain-enqueuer.ts",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"to": "src/hooks/session-end/cleanup-manifest.ts",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"to": "src/hooks/session-end/foreground-bootstrap.ts",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/foreground-bootstrap.test.ts",
|
||||
"to": "src/hooks/session-end/worker.ts",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/__tests__/guardrails.test.ts",
|
||||
"to": "external:fs",
|
||||
@@ -64577,6 +64629,11 @@
|
||||
"to": "src/lib/worktree-paths.ts",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/foreground-bootstrap.ts",
|
||||
"to": "src/hooks/session-end/chain-enqueuer.ts",
|
||||
"kind": "imports"
|
||||
},
|
||||
{
|
||||
"from": "src/hooks/session-end/foreground-bootstrap.ts",
|
||||
"to": "src/hooks/session-end/cleanup-manifest.ts",
|
||||
@@ -82604,12 +82661,12 @@
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"nodeCount": 7121,
|
||||
"edgeCount": 8093,
|
||||
"importEdgeCount": 6681,
|
||||
"nodeCount": 7122,
|
||||
"edgeCount": 8103,
|
||||
"importEdgeCount": 6691,
|
||||
"registerEdgeCount": 68
|
||||
}
|
||||
},
|
||||
"inventorySha256": "b631163acbd3652003f89ebfcca76c81a69633ffa707d8e3269fd1fe83892a49",
|
||||
"manifestSha256": "6bf8d25a67b6bdca18f1950bc5915f4825a7cbfab5c9665a377a9225c9e8e205"
|
||||
"inventorySha256": "ab6da542a11531649af7965ae0b3a575b88ab49eb2c4eb2c865bc6ad97f425b9",
|
||||
"manifestSha256": "5069b758519b84e68cb6ed5ac423646bf28f235be080b29cab0a58bbc09a6788"
|
||||
}
|
||||
@@ -72,9 +72,10 @@ describe('chain-enqueuer SessionEnd registration (factory third link)', () => {
|
||||
expect(existsSync(templatePath)).toBe(true);
|
||||
|
||||
const source = readFileSync(templatePath, 'utf8');
|
||||
// Must delegate to --hook=session-end (bridge), the only entry that runs
|
||||
// processSessionEnd → planChainEnqueue. publishSessionEndBootstrap (the
|
||||
// plugin scripts/session-end.mjs path) does not enqueue chains.
|
||||
// Must delegate to --hook=session-end (bridge), which runs
|
||||
// processSessionEnd → planChainEnqueue. The plugin path
|
||||
// (scripts/session-end.mjs → publishSessionEndBootstrap) enqueues the
|
||||
// chain itself in foreground-bootstrap.ts.
|
||||
expect(source).toContain("'--hook=session-end'");
|
||||
expect(source).toContain('bridge');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Tests for the plugin-path SessionEnd bootstrap chain wiring (defect: plugin
|
||||
* installs register hooks/hooks.json → scripts/session-end.mjs →
|
||||
* publishSessionEndBootstrap, which is the ONLY SessionEnd entry when plugin
|
||||
* hooks are enabled — the standalone settings.json forwarder is skipped).
|
||||
* The bootstrap must therefore plan the chain enqueue and merge it into the
|
||||
* durable manifest payload exactly like processSessionEnd does.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, afterEach, vi } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
vi.mock('../worker.js', () => ({ spawnSessionEndWorker: vi.fn(() => true) }));
|
||||
|
||||
import { publishSessionEndBootstrap } from '../foreground-bootstrap.js';
|
||||
import { readSessionEndJob } from '../cleanup-manifest.js';
|
||||
import { factoryStateDir } from '../chain-enqueuer.js';
|
||||
import { spawnSessionEndWorker } from '../worker.js';
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
function tempDir(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'omc-foreground-bootstrap-'));
|
||||
tempRoots.push(dir);
|
||||
execFileSync('git', ['init', '--quiet'], { cwd: dir, stdio: 'ignore' });
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempRoots) rmSync(dir, { recursive: true, force: true });
|
||||
tempRoots.length = 0;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function writeLedger(directory: string, sessionId: string, ledger: Record<string, unknown>): void {
|
||||
mkdirSync(factoryStateDir(directory), { recursive: true });
|
||||
writeFileSync(join(factoryStateDir(directory), `chain-${sessionId}.json`), JSON.stringify(ledger), 'utf8');
|
||||
}
|
||||
|
||||
function bootstrapInput(directory: string, sessionId: string) {
|
||||
return {
|
||||
session_id: sessionId,
|
||||
transcript_path: '',
|
||||
cwd: directory,
|
||||
permission_mode: '',
|
||||
hook_event_name: 'SessionEnd' as const,
|
||||
reason: 'prompt_input_exit' as const,
|
||||
};
|
||||
}
|
||||
|
||||
describe('publishSessionEndBootstrap chain wiring (plugin session-end path)', () => {
|
||||
it('merges the planned chain into the durable manifest payload when a ledger exists', async () => {
|
||||
const dir = tempDir();
|
||||
writeLedger(dir, 'sess-a', { intentId: 'intent-a' });
|
||||
mkdirSync(join(dir, '.omc'), { recursive: true });
|
||||
writeFileSync(join(dir, '.omc', 'factory-routes.json'), JSON.stringify({
|
||||
'success:*': { stage: 'review', skill: 'code-review' },
|
||||
}), 'utf8');
|
||||
|
||||
const result = await publishSessionEndBootstrap(bootstrapInput(dir, 'sess-a'));
|
||||
|
||||
expect(result).toEqual({ continue: true });
|
||||
const manifest = readSessionEndJob(dir, 'sess-a');
|
||||
expect(manifest).not.toBeNull();
|
||||
const chain = manifest?.actions['spawn-next']?.payload?.chain as Record<string, unknown> | undefined;
|
||||
expect(chain).toBeDefined();
|
||||
expect(chain?.outcome).toBe('success');
|
||||
expect(chain?.intentId).toBe('intent-a');
|
||||
// The detached worker must be launched so the spawn-next action executes.
|
||||
expect(spawnSessionEndWorker).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('records no chain payload when the session has no factory ledger', async () => {
|
||||
const dir = tempDir();
|
||||
await publishSessionEndBootstrap(bootstrapInput(dir, 'sess-b'));
|
||||
const manifest = readSessionEndJob(dir, 'sess-b');
|
||||
expect(manifest).not.toBeNull();
|
||||
expect(manifest?.actions['spawn-next']?.payload?.chain).toBeUndefined();
|
||||
expect(spawnSessionEndWorker).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { prepareCoreManifest } from './cleanup-manifest.js';
|
||||
import { planChainEnqueue } from './chain-enqueuer.js';
|
||||
import { resolveToWorktreeRoot, validateSessionId } from '../../lib/worktree-paths.js';
|
||||
|
||||
export interface SessionEndBootstrapInput { session_id: string; transcript_path: string; cwd: string; permission_mode: string; hook_event_name: 'SessionEnd'; reason: 'clear' | 'logout' | 'prompt_input_exit' | 'other'; }
|
||||
@@ -15,7 +16,13 @@ export interface SessionEndBootstrapResult { continue: true; }
|
||||
export async function publishSessionEndBootstrap(input: SessionEndBootstrapInput): Promise<SessionEndBootstrapResult> {
|
||||
validateSessionId(input.session_id);
|
||||
const directory = resolveToWorktreeRoot(input.cwd);
|
||||
const payload = { transcriptPath: input.transcript_path, cwd: input.cwd, reason: input.reason, input, initialTeamNames: [] };
|
||||
// Plugin installs route SessionEnd here (hooks/hooks.json → scripts/session-end.mjs),
|
||||
// so the chain enqueue must happen on this path — the standalone settings.json
|
||||
// forwarder (processSessionEnd) is not registered when plugin hooks are enabled.
|
||||
const chain = planChainEnqueue(directory, input.session_id, input.reason);
|
||||
const payload = chain
|
||||
? { transcriptPath: input.transcript_path, cwd: input.cwd, reason: input.reason, input, initialTeamNames: [], chain }
|
||||
: { transcriptPath: input.transcript_path, cwd: input.cwd, reason: input.reason, input, initialTeamNames: [] };
|
||||
const prepared = prepareCoreManifest(directory, input.session_id, payload);
|
||||
if (prepared) {
|
||||
const { spawnSessionEndWorker } = await import('./worker.js');
|
||||
|
||||
Reference in new issue
Block a user