* test(#2752): path is authoritative over model-controlled file_path in phase-boundary hook Rewrite the #2304 precedence test (which pinned the buggy file_path-wins behavior with an impossible no-tool_name {file_path,path} payload) to assert the correct precedence with realistic Kimi-shaped payloads: a real .planning/ write with a decoy file_path must still fire the reminder (suppression repro), and a write elsewhere with a decoy .planning/ file_path must NOT fabricate one (fabrication repro). Update the parity vocabulary alarm to the new expression. * fix(#2752): make tool_input.path authoritative over model-controlled file_path in gsd-phase-boundary.sh The hook consulted file_path first, path second. kimi-cli executes on path and sends path only; file_path on a Kimi payload is always model-supplied. So a model-supplied decoy file_path could suppress the reminder for a real .planning/ write or fabricate one for a file never touched. Flip the precedence so path is authoritative and file_path is the fallback (Claude Code emits file_path and no path, so the fallback must remain). Mirrors the #2595 JS-guard fix. * chore(#2752): changeset fragment * chore(#2752): clarify comment/changeset — JS guards use upstream normalization, shell hook applies precedence directly (review minor 1) * chore(#2752): backfill changeset PR number (2860) --------- Co-authored-by: Test <test@example.com>
60 lines
2.8 KiB
Bash
Executable File
60 lines
2.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gsd-hook-version: {{GSD_VERSION}}
|
|
# gsd-phase-boundary.sh — PostToolUse hook: detect .planning/ file writes
|
|
# Outputs a reminder when planning files are modified outside normal workflow.
|
|
# Uses Node.js for JSON parsing (always available in GSD projects, no jq dependency).
|
|
#
|
|
# OPT-IN: This hook is a no-op unless config.json has hooks.community: true.
|
|
# Enable with: "hooks": { "community": true } in .planning/config.json
|
|
|
|
# Check opt-in config — exit silently if not enabled
|
|
if [ -f .planning/config.json ]; then
|
|
ENABLED=$(node -e "try{const c=require('./.planning/config.json');process.stdout.write(c.hooks?.community===true?'1':'0')}catch{process.stdout.write('0')}" 2>/dev/null)
|
|
if [ "$ENABLED" != "1" ]; then exit 0; fi
|
|
else
|
|
exit 0
|
|
fi
|
|
|
|
INPUT=$(cat)
|
|
|
|
# Extract file_path from JSON using Node (handles escaping correctly).
|
|
# #2304: Kimi CLI registers this hook with matcher 'WriteFile|StrReplaceFile'
|
|
# and its file tools name the field `path`, not `file_path` (kimi-cli
|
|
# src/kimi_cli/tools/file/write.py + replace.py) — fall back to tool_input.path
|
|
# when file_path is absent, mirroring normalizeKimiPayload in the JS guards.
|
|
# #2752: `path` is AUTHORITATIVE (kimi-cli executes on it; it sends `path` only,
|
|
# never `file_path`). `file_path` is model-controlled on Kimi, so consulting it
|
|
# first let a model-supplied decoy suppress/fabricate the reminder. `path` wins,
|
|
# `file_path` is the fallback (Claude Code emits `file_path` and no `path`, so the
|
|
# fallback must remain). The JS guards reach the same "path authoritative" outcome
|
|
# via an upstream normalizeKimiPayload step (copies path→file_path before any guard
|
|
# reads); this shell hook parses tool_input once, raw, so it applies the precedence
|
|
# directly at the read site.
|
|
FILE=$(echo "$INPUT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d).tool_input||{};process.stdout.write((typeof i.path==='string'&&i.path)||(typeof i.file_path==='string'&&i.file_path)||'')}catch{}})" 2>/dev/null)
|
|
|
|
# Emit a structured JSON envelope (#2974). additionalContext carries the
|
|
# user-visible reminder text; the typed `planning_modified` boolean and
|
|
# `file_path` let tests assert on the structured contract without grepping.
|
|
PLANNING_MODIFIED="false"
|
|
if [[ "$FILE" == *.planning/* ]] || [[ "$FILE" == .planning/* ]]; then
|
|
PLANNING_MODIFIED="true"
|
|
fi
|
|
|
|
if [ "$PLANNING_MODIFIED" = "true" ]; then
|
|
node -e '
|
|
const file = process.argv[1];
|
|
const additionalContext = ".planning/ file modified: " + file + "\n" +
|
|
"Check: Should STATE.md be updated to reflect this change?";
|
|
process.stdout.write(JSON.stringify({
|
|
hookSpecificOutput: {
|
|
hookEventName: "PostToolUse",
|
|
additionalContext,
|
|
planning_modified: true,
|
|
file_path: file,
|
|
},
|
|
}));
|
|
' "$FILE"
|
|
fi
|
|
|
|
exit 0
|