Skip to content

Commit 1efef96

Browse files
author
catlog22
committed
fix(hook): update CCW Coordinator Tracker to use 'Stop' trigger and improve next-step hint injection
fix(install): enhance installation prompt with detailed CCW hook descriptions docs(workflow-tune): clarify requirement for absolute paths in sandbox creation
1 parent d4fe81c commit 1efef96

4 files changed

Lines changed: 134 additions & 54 deletions

File tree

.claude/commands/workflow-tune.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Input → Parse → GenTestTask → Confirm → Setup
3131
- 错误: 功能点="撰写 Introduction (TASK-001)",验收标准="introduction.md 存在" ← 仅完成 1/N
3232
4. **Sandbox Isolation**. 全部执行在 `sandbox/` 目录(独立 git 仓库),不影响真实项目。
3333
5. **State Machine**. 通过 `current_step` + `current_phase` 推进,禁止同步循环。
34+
6. **ABSOLUTE PATHS for --cd**. `ccw cli --cd` 必须使用绝对路径。相对路径会被 ccw cli 再次拼接 CWD 导致路径重复。`workDir`/`sandboxDir` 在创建时就解析为绝对路径。
3435

3536
## Input Formats
3637

@@ -134,8 +135,9 @@ for (const [stepIdx, step] of stepsNeedTask.entries()) {
134135
const commandDoc = generateCommandDoc(steps, workflowName, projectScenario, analysisDepth);
135136
if (!autoYes) { /* AskUserQuestion: confirm or cancel */ }
136137

137-
// Create sandbox
138-
const workDir = `.workflow/.scratchpad/workflow-tune-${Date.now()}`;
138+
// Create sandbox — MUST resolve absolute path (P0 Rule #6)
139+
const cwd = Bash('pwd').stdout.trim();
140+
const workDir = `${cwd}/.workflow/.scratchpad/workflow-tune-${Date.now()}`;
139141
const sandboxDir = `${workDir}/sandbox`;
140142
Bash(`mkdir -p "${workDir}/steps" "${sandboxDir}"`);
141143
Bash(`cd "${sandboxDir}" && git init && echo "# Sandbox" > README.md && git add . && git commit -m "init"`);

ccw/src/commands/hook.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ async function notifyAction(options: HookOptions): Promise<void> {
730730
/**
731731
* CCW Coordinator Tracker action - track /ccw and /ccw-coordinator progress
732732
*
733-
* PostToolUse hook: reads CCW status.json, writes bridge file, injects next-step hints.
733+
* Stop hook: reads CCW status.json on response end, writes bridge file.
734734
*/
735735
async function ccwCoordinatorTrackerAction(options: HookOptions): Promise<void> {
736736
const { stdin } = options;
@@ -755,11 +755,10 @@ async function ccwCoordinatorTrackerAction(options: HookOptions): Promise<void>
755755
try {
756756
const workspace = getProjectPath(hookData.cwd);
757757

758-
const { readLatestCcwSession, readCoordBridge, writeCoordBridge, buildNextStepHint } =
758+
const { readLatestCcwSession, writeCoordBridge, buildNextStepHint } =
759759
await import('../core/hooks/ccw-coordinator-tracker.js');
760760

761-
const existing = readCoordBridge(sessionId);
762-
const bridgeData = readLatestCcwSession(workspace, existing);
761+
const bridgeData = readLatestCcwSession(workspace);
763762
if (!bridgeData) {
764763
process.exit(0);
765764
}
@@ -770,12 +769,7 @@ async function ccwCoordinatorTrackerAction(options: HookOptions): Promise<void>
770769
// Inject next-step hint for active sessions
771770
const hint = buildNextStepHint(bridgeData);
772771
if (hint) {
773-
process.stdout.write(JSON.stringify({
774-
hookSpecificOutput: {
775-
hookEventName: 'PostToolUse',
776-
additionalContext: hint,
777-
},
778-
}));
772+
process.stdout.write(JSON.stringify({ continue: true, message: hint }));
779773
}
780774

781775
process.exit(0);

ccw/src/commands/install.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,11 @@ export async function installCommand(options: InstallOptions): Promise<void> {
347347
message: 'Install Claude Code hooks?',
348348
choices: [
349349
{
350-
name: `${chalk.cyan('Full')} — coordinator-tracker + monitoring hooks (recommended)`,
350+
name: [
351+
`${chalk.cyan('Full')} — install CCW hooks to settings.json (recommended)`,
352+
chalk.gray(' ccw-coordinator-tracker Stop — check /ccw chain progress on response end, write bridge'),
353+
chalk.gray(' ccw-coordinator-skill-context UserPromptSubmit — show active coordinator progress on /ccw input'),
354+
].join('\n'),
351355
value: 'full'
352356
},
353357
{
@@ -1520,8 +1524,6 @@ function installCcwHooks(settingsDir: string): string[] {
15201524
const templates = [
15211525
'ccw-coordinator-tracker', // PostToolUse — track /ccw and /ccw-coordinator progress
15221526
'ccw-coordinator-skill-context', // UserPromptSubmit — inject progress hints when invoking /ccw
1523-
'stop-notify', // Stop — notify dashboard on response completion
1524-
'memory-v2-extract', // Stop — trigger memory extraction on session end
15251527
];
15261528

15271529
const installed: string[] = [];

ccw/src/core/hooks/hook-templates.ts

Lines changed: 121 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -770,40 +770,101 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
770770
name: 'CCW Coordinator Tracker',
771771
description: 'Track /ccw and /ccw-coordinator execution progress, inject next-step hints when paused',
772772
category: 'automation',
773-
trigger: 'PostToolUse',
773+
trigger: 'Stop',
774774
execute: (data) => {
775775
const sessionId = getStringInput(data.session_id);
776776
if (!sessionId) return { exitCode: 0 };
777777

778778
const workspace = data.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
779779

780-
// Dynamic import to avoid circular dependencies
781780
try {
782-
const { readLatestCcwSession, readCoordBridge, writeCoordBridge, buildNextStepHint } =
783-
require('../hooks/ccw-coordinator-tracker.js');
784-
785-
const existing = readCoordBridge(sessionId);
786-
const bridgeData = readLatestCcwSession(workspace, existing);
787-
if (!bridgeData) return { exitCode: 0 };
781+
const { existsSync, readFileSync, writeFileSync, readdirSync, statSync } = require('fs') as typeof import('fs');
782+
const { join } = require('path') as typeof import('path');
783+
const { tmpdir } = require('os') as typeof import('os');
784+
const CCW_COORD_BRIDGE_PREFIX = 'ccw-coord-';
785+
786+
// --- Inline: read latest CCW session ---
787+
function readLatestSession(dir: string, subPath: string): { bridge: CcwBridgeData; mtime: number } | null {
788+
const base = join(dir, subPath);
789+
if (!existsSync(base)) return null;
790+
try {
791+
const sessions = readdirSync(base)
792+
.map(name => {
793+
const sp = join(base, name, 'state.json');
794+
if (!existsSync(sp)) {
795+
// ccw uses status.json, ccw-coordinator uses state.json
796+
return null;
797+
}
798+
const mtime = statSync(sp).mtimeMs;
799+
const raw = JSON.parse(readFileSync(sp, 'utf8'));
800+
const chain = raw.command_chain ?? [];
801+
const results = raw.execution_results ?? [];
802+
const ci = results.findIndex((r: any) => r.status === 'in-progress');
803+
const completed = results.filter((r: any) => r.status === 'completed').length;
804+
const current = ci >= 0 && chain[ci] ? chain[ci].command : null;
805+
const ni = ci >= 0 ? ci + 1 : chain.length;
806+
const next = chain[ni] ? chain[ni].command : null;
807+
return {
808+
bridge: {
809+
session_id: sessionId, coordinator: subPath.includes('ccw-coordinator') ? 'ccw-coordinator' : 'ccw',
810+
chain_name: raw.workflow ?? '', intent: raw.analysis?.goal ?? '',
811+
steps_total: chain.length, steps_completed: completed,
812+
current_step: current != null ? { index: ci, command: current } : null,
813+
next_step: next != null ? { index: ni, command: next } : null,
814+
remaining_steps: chain.slice(ni).map((s: any) => ({ command: s.command ?? '' })),
815+
status: raw.status ?? 'unknown', updated_at: Math.floor(mtime),
816+
},
817+
mtime,
818+
};
819+
})
820+
.filter((s): s is NonNullable<typeof s> => s !== null)
821+
.sort((a, b) => b.mtime - a.mtime);
822+
if (sessions.length === 0) return null;
823+
return sessions[0];
824+
} catch { return null; }
825+
}
788826

789-
bridgeData.session_id = sessionId;
790-
writeCoordBridge(sessionId, bridgeData);
827+
interface CcwBridgeData {
828+
session_id: string; coordinator: string; chain_name: string; intent: string;
829+
steps_total: number; steps_completed: number;
830+
current_step: { index: number; command: string } | null;
831+
next_step: { index: number; command: string } | null;
832+
remaining_steps: Array<{ command: string }>; status: string; updated_at: number;
833+
}
791834

792-
// Inject next-step hint for active sessions
793-
const hint = buildNextStepHint(bridgeData);
794-
if (hint) {
795-
return {
796-
exitCode: 0,
797-
jsonOutput: {
798-
hookSpecificOutput: {
799-
hookEventName: 'PostToolUse',
800-
additionalContext: hint,
801-
},
802-
},
803-
};
835+
const ccwResult = readLatestSession(workspace, '.workflow/.ccw');
836+
const coordResult = readLatestSession(workspace, '.workflow/.ccw-coordinator');
837+
const candidates = [ccwResult, coordResult].filter((s): s is NonNullable<typeof s> => s !== null);
838+
if (candidates.length === 0) return { exitCode: 0 };
839+
840+
const best = candidates.sort((a, b) => b.mtime - a.mtime)[0];
841+
842+
// Write bridge file
843+
const bridgePath = join(tmpdir(), `${CCW_COORD_BRIDGE_PREFIX}${sessionId}.json`);
844+
writeFileSync(bridgePath, JSON.stringify(best.bridge));
845+
846+
// Build next-step hint for active sessions
847+
if (best.bridge.status === 'running' || best.bridge.status === 'waiting') {
848+
if (best.bridge.next_step) {
849+
const p = `[${best.bridge.steps_completed}/${best.bridge.steps_total}]`;
850+
const lines = [
851+
`## CCW Coordinator Active`,
852+
`Chain: ${best.bridge.chain_name} ${p} | Status: ${best.bridge.status}`,
853+
`Last: ${best.bridge.current_step?.command ?? '(unknown)'}`,
854+
`Next: ${best.bridge.next_step.command}`,
855+
];
856+
if (best.bridge.remaining_steps.length > 1) {
857+
const r = best.bridge.remaining_steps.slice(1, 4).map(s => s.command).join(' → ');
858+
lines.push(`Then: ${r}${best.bridge.remaining_steps.length > 4 ? ' …' : ''}`);
859+
}
860+
return {
861+
exitCode: 0,
862+
jsonOutput: { continue: true, message: lines.join('\n') },
863+
};
864+
}
804865
}
805866
} catch {
806-
// Silent fail — tracker must not break tool execution
867+
// Silent fail — tracker must not break hook execution
807868
}
808869
return { exitCode: 0 };
809870
}
@@ -826,24 +887,45 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
826887
if (!sessionId) return { exitCode: 0 };
827888

828889
try {
829-
const { readCoordBridge, buildNextStepHint } =
830-
require('../hooks/ccw-coordinator-tracker.js');
831-
832-
const bridgeData = readCoordBridge(sessionId);
833-
if (!bridgeData) return { exitCode: 0 };
890+
const { existsSync, readFileSync } = require('fs') as typeof import('fs');
891+
const { join } = require('path') as typeof import('path');
892+
const { tmpdir } = require('os') as typeof import('os');
893+
894+
// Read bridge file
895+
const bridgePath = join(tmpdir(), `ccw-coord-${sessionId}.json`);
896+
if (!existsSync(bridgePath)) return { exitCode: 0 };
897+
898+
const bridge: {
899+
status: string; steps_total: number; steps_completed: number;
900+
chain_name: string; current_step: { command: string } | null; next_step: { command: string } | null;
901+
remaining_steps: Array<{ command: string }>;
902+
} = JSON.parse(readFileSync(bridgePath, 'utf8'));
903+
904+
// Only inject for active sessions with a next step
905+
const isActive = bridge.status === 'running' || bridge.status === 'waiting';
906+
if (!isActive || !bridge.next_step) return { exitCode: 0 };
907+
908+
const p = `[${bridge.steps_completed}/${bridge.steps_total}]`;
909+
const lines = [
910+
`## CCW Coordinator Active`,
911+
`Chain: ${bridge.chain_name} ${p} | Status: ${bridge.status}`,
912+
`Last: ${bridge.current_step?.command ?? '(unknown)'}`,
913+
`Next: ${bridge.next_step.command}`,
914+
];
915+
if (bridge.remaining_steps.length > 1) {
916+
const r = bridge.remaining_steps.slice(1, 4).map(s => s.command).join(' → ');
917+
lines.push(`Then: ${r}${bridge.remaining_steps.length > 4 ? ' …' : ''}`);
918+
}
834919

835-
const hint = buildNextStepHint(bridgeData);
836-
if (hint) {
837-
return {
838-
exitCode: 0,
839-
jsonOutput: {
840-
hookSpecificOutput: {
841-
hookEventName: 'UserPromptSubmit',
842-
additionalContext: hint,
843-
},
920+
return {
921+
exitCode: 0,
922+
jsonOutput: {
923+
hookSpecificOutput: {
924+
hookEventName: 'UserPromptSubmit',
925+
additionalContext: lines.join('\n'),
844926
},
845-
};
846-
}
927+
},
928+
};
847929
} catch {
848930
// Silent fail
849931
}

0 commit comments

Comments
 (0)