oh-my-customcode 1.1.60 → 1.1.62
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.
- package/dist/cli/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/.claude/hooks/scripts/agent-start-recorder.sh +4 -0
- package/templates/.claude/hooks/scripts/agent-teams-advisor.sh +6 -2
- package/templates/.claude/hooks/scripts/audit-log.sh +19 -2
- package/templates/.claude/hooks/scripts/content-hash-validator.sh +6 -2
- package/templates/.claude/hooks/scripts/model-escalation-advisor.sh +6 -2
- package/templates/.claude/hooks/scripts/playwright-compress.sh +25 -2
- package/templates/.claude/hooks/scripts/r007-r008-drift-advisor.sh +72 -2
- package/templates/.claude/hooks/scripts/schema-validator.sh +14 -9
- package/templates/.claude/hooks/scripts/secret-filter.sh +46 -4
- package/templates/.claude/hooks/scripts/session-reflection.sh +5 -1
- package/templates/.claude/hooks/scripts/stuck-detector.sh +43 -11
- package/templates/.claude/hooks/scripts/task-outcome-recorder.sh +16 -9
- package/templates/.claude/rules/MUST-completion-verification.md +4 -0
- package/templates/.claude/rules/MUST-enforcement-policy.md +1 -1
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +2 -0
- package/templates/.claude/rules/MUST-sync-verification.md +4 -0
- package/templates/.claude/rules/MUST-tool-identification.md +11 -0
- package/templates/.claude/rules/SHOULD-memory-integration.md +3 -0
- package/templates/.claude/skills/fsd/SKILL.md +40 -2
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +38 -5
- package/templates/.claude/skills/post-release-followup/SKILL.md +8 -0
- package/templates/manifest.json +1 -1
- package/templates/workflows/auto-dev.yaml +38 -5
package/dist/cli/index.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -10,6 +10,10 @@ command -v jq >/dev/null 2>&1 || { cat; exit 0; }
|
|
|
10
10
|
|
|
11
11
|
input=$(cat)
|
|
12
12
|
|
|
13
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
14
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
15
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
16
|
+
|
|
13
17
|
agent_type=$(printf '%s\n' "$input" | jq -r '.agent_type // "unknown"')
|
|
14
18
|
model=$(printf '%s\n' "$input" | jq -r '.model // "inherit"')
|
|
15
19
|
description=$(printf '%s\n' "$input" | jq -r '.description // ""' | head -c 80)
|
|
@@ -11,6 +11,10 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
11
11
|
|
|
12
12
|
input=$(cat)
|
|
13
13
|
|
|
14
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
15
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
16
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
17
|
+
|
|
14
18
|
# Skip unless Agent Teams is CONFIRMED active.
|
|
15
19
|
# session-env-check.sh writes one of: disabled | env-set | enabled (#1588).
|
|
16
20
|
# `env-set` = the env var is set but TeamCreate presence is UNVERIFIED — R018 is dormant,
|
|
@@ -43,8 +47,8 @@ if [ -f "$RELEASE_PLAN" ]; then
|
|
|
43
47
|
fi
|
|
44
48
|
|
|
45
49
|
# Extract task info from input
|
|
46
|
-
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // "unknown"')
|
|
47
|
-
prompt_preview=$(printf '%s\n' "$input" | jq -r '.tool_input.description // ""' | head -c 60)
|
|
50
|
+
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // "unknown"')
|
|
51
|
+
prompt_preview=$(printf '%s\n' "$input" | jq -r '.tool_input.description? // ""' | head -c 60)
|
|
48
52
|
|
|
49
53
|
# Session-scoped counter using parent PID as session identifier
|
|
50
54
|
COUNTER_FILE="/tmp/.claude-task-count-${PPID}"
|
|
@@ -13,12 +13,29 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
13
13
|
|
|
14
14
|
input=$(cat)
|
|
15
15
|
|
|
16
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
17
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
18
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
19
|
+
|
|
16
20
|
# Extract fields from hook input
|
|
17
21
|
tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name // "unknown"')
|
|
18
|
-
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path // .tool_input.command // ""' | head -c 200)
|
|
22
|
+
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path? // .tool_input.command? // ""' | head -c 200)
|
|
19
23
|
agent_type=$(printf '%s\n' "$input" | jq -r '.agent_type // "unknown"')
|
|
20
24
|
model=$(printf '%s\n' "$input" | jq -r '.model // "unknown"')
|
|
21
|
-
|
|
25
|
+
# Outcome signal. PostToolUse carries the result under `tool_response`, not
|
|
26
|
+
# `tool_output` (measured 2026-09-03: 1764/1764 PostToolUse payloads had
|
|
27
|
+
# `tool_response`, 0/1764 had `tool_output`), so the old `.tool_output.is_error`
|
|
28
|
+
# read was always absent and every entry was logged as outcome=success.
|
|
29
|
+
# Measured failure signals: `.tool_response.is_error` (generic) and
|
|
30
|
+
# `.tool_response.interrupted` (present on every Bash response).
|
|
31
|
+
# jq's `//` treats both null and false as absent, so this chain is an OR:
|
|
32
|
+
# the result is true iff at least one signal is true.
|
|
33
|
+
is_error=$(printf '%s\n' "$input" | jq -r '
|
|
34
|
+
(.tool_response?.is_error?
|
|
35
|
+
// .tool_response?.interrupted?
|
|
36
|
+
// .tool_output?.is_error?
|
|
37
|
+
// false) | tostring
|
|
38
|
+
' 2>/dev/null) || is_error="false"
|
|
22
39
|
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
23
40
|
|
|
24
41
|
# Determine outcome
|
|
@@ -12,6 +12,10 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
12
12
|
|
|
13
13
|
input=$(cat)
|
|
14
14
|
|
|
15
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
16
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
17
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
15
19
|
# Hash store (PPID-scoped, session-only)
|
|
16
20
|
HASH_STORE="/tmp/.claude-content-hashes-${PPID}"
|
|
17
21
|
|
|
@@ -20,7 +24,7 @@ tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name // "unknown"')
|
|
|
20
24
|
case "$tool_name" in
|
|
21
25
|
"Read")
|
|
22
26
|
# Store content hash for the file that was just read
|
|
23
|
-
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path // ""')
|
|
27
|
+
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path? // ""')
|
|
24
28
|
|
|
25
29
|
if [ -n "$file_path" ] && [ -f "$file_path" ]; then
|
|
26
30
|
content_hash=$(md5 -q "$file_path" 2>/dev/null || md5sum "$file_path" 2>/dev/null | cut -d' ' -f1 || echo "unknown")
|
|
@@ -43,7 +47,7 @@ case "$tool_name" in
|
|
|
43
47
|
|
|
44
48
|
"Edit")
|
|
45
49
|
# Validate that file hasn't changed since last Read
|
|
46
|
-
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path // ""')
|
|
50
|
+
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path? // ""')
|
|
47
51
|
|
|
48
52
|
if [ -n "$file_path" ] && [ -f "$HASH_STORE" ] && [ -f "$file_path" ]; then
|
|
49
53
|
stored_hash=$(grep "\"path\":\"${file_path}\"" "$HASH_STORE" 2>/dev/null | tail -1 | jq -r '.hash // ""' 2>/dev/null || echo "")
|
|
@@ -11,9 +11,13 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
11
11
|
|
|
12
12
|
input=$(cat)
|
|
13
13
|
|
|
14
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
15
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
16
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
17
|
+
|
|
14
18
|
# Extract current task info
|
|
15
|
-
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // "unknown"')
|
|
16
|
-
current_model=$(printf '%s\n' "$input" | jq -r '.tool_input.model // "inherit"')
|
|
19
|
+
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // "unknown"')
|
|
20
|
+
current_model=$(printf '%s\n' "$input" | jq -r '.tool_input.model? // "inherit"')
|
|
17
21
|
|
|
18
22
|
# Session-scoped outcome log
|
|
19
23
|
OUTCOME_FILE="/tmp/.claude-task-outcomes-${PPID}"
|
|
@@ -7,7 +7,30 @@
|
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
|
|
9
9
|
input=$(cat)
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
12
|
+
# Hooks must never crash (R021); jq parse errors would otherwise abort under `set -e`. (#1650)
|
|
13
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
14
|
+
# PostToolUse carries the tool result under `tool_response`, not `tool_output`
|
|
15
|
+
# (measured 2026-09-03: 1764/1764 PostToolUse payloads had `tool_response`,
|
|
16
|
+
# 0/1764 had `tool_output`), so the previous `.tool_output` read always yielded
|
|
17
|
+
# "" and this hook never compressed anything. MCP responses arrive either as a
|
|
18
|
+
# bare string or as `{content: [{type:"text", text:...}]}`; `.tool_output` is
|
|
19
|
+
# kept as a fallback for events still using the older shape.
|
|
20
|
+
tool_output=$(printf '%s\n' "$input" | jq -r '
|
|
21
|
+
[
|
|
22
|
+
(.tool_response? | if type == "string" then . else empty end),
|
|
23
|
+
(.tool_response?.content? | strings),
|
|
24
|
+
(.tool_response?.content? | if type == "array"
|
|
25
|
+
then map(.text? | strings) | join("\n")
|
|
26
|
+
else empty end),
|
|
27
|
+
(.tool_response?.stdout? | strings),
|
|
28
|
+
(.tool_response?.file?.content? | strings),
|
|
29
|
+
(.tool_output? | if type == "string" then . else empty end),
|
|
30
|
+
(.tool_output?.output? | strings)
|
|
31
|
+
]
|
|
32
|
+
| map(select(. != "")) | join("\n")
|
|
33
|
+
' 2>/dev/null) || tool_output=""
|
|
11
34
|
|
|
12
35
|
# Skip if output is small (< 3000 chars)
|
|
13
36
|
output_len=${#tool_output}
|
|
@@ -47,4 +70,4 @@ fi
|
|
|
47
70
|
compressed_len=${#summary}
|
|
48
71
|
savings=$(( (output_len - compressed_len) * 100 / output_len ))
|
|
49
72
|
printf '%s\n' "$input" | jq --arg summary "$summary" --arg savings "${savings}% reduced (${output_len}→${compressed_len} chars)" \
|
|
50
|
-
'.tool_output = $summary | .["updatedMCPToolOutput"] = $summary'
|
|
73
|
+
'.tool_response = $summary | .tool_output = $summary | .["updatedMCPToolOutput"] = $summary'
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
#
|
|
23
23
|
# Advisory-only: ALWAYS exits 0, NEVER blocks.
|
|
24
24
|
#
|
|
25
|
+
# Scope gate (#1650 D): the advisor judges the ORCHESTRATOR's turns only. When the hook fires
|
|
26
|
+
# from INSIDE a subagent session (the payload carries `agent_id`) it exits immediately — see
|
|
27
|
+
# the measured rationale at the gate itself. SubagentStart/SubagentStop are exempt because
|
|
28
|
+
# `agent_id` is a required TARGET identifier there, not an in-subagent marker.
|
|
29
|
+
#
|
|
25
30
|
# ── Transcript schema (MEASURED 2026-08-05, #1553) ────────────────────────────────────
|
|
26
31
|
# Claude Code JSONL lines do NOT carry a TOP-LEVEL `role`/`content`. The measured top-level
|
|
27
32
|
# key set is:
|
|
@@ -134,15 +139,58 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
134
139
|
fi
|
|
135
140
|
|
|
136
141
|
# ── 입력 필드 추출 (jq 1회 fork) ──
|
|
137
|
-
|
|
142
|
+
# stdin 가드: 비-JSON이면 jq가 실패해 `|| exit 0`으로 무음 종료하고, 유효 JSON이지만 객체가
|
|
143
|
+
# 아니면(배열/문자열/숫자) `select(type == "object")`가 걸러 빈 출력 → session_id 공백 → exit 0.
|
|
144
|
+
# 두 경로 모두 추가 fork 없이 기존 1회 fork 안에서 처리한다(PostToolUse는 매 도구 호출마다 발화).
|
|
145
|
+
meta=$(printf '%s' "$input" | jq -r 'select(type == "object") | [(.session_id // ""), (.hook_event_name // ""), (.transcript_path // ""), (.agent_id // "")] | @tsv' 2>/dev/null) || exit 0
|
|
138
146
|
session_id=$(printf '%s' "$meta" | cut -f1)
|
|
139
147
|
hook_event_name=$(printf '%s' "$meta" | cut -f2)
|
|
140
148
|
transcript_in=$(printf '%s' "$meta" | cut -f3)
|
|
149
|
+
agent_id=$(printf '%s' "$meta" | cut -f4)
|
|
141
150
|
|
|
142
151
|
if [ -z "$session_id" ]; then
|
|
143
152
|
exit 0
|
|
144
153
|
fi
|
|
145
154
|
|
|
155
|
+
# ── 서브에이전트 세션이면 즉시 종료 (#1650 D) ────────────────────────────────────────────
|
|
156
|
+
# settings 훅은 서브에이전트 세션에서도 발화한다(서브에이전트의 매 도구 호출마다 PostToolUse).
|
|
157
|
+
# 그 발화가 남기는 advisory 피드백이 서브에이전트의 마지막 턴들을 잠식해 완료 보고를 대체하는
|
|
158
|
+
# 현상이 관측됐다(R020 8항 — 훅 피드백 잠식).
|
|
159
|
+
#
|
|
160
|
+
# 판별 신호 실측 (CC 2.1.259 바이너리의 훅 입력 zod 스키마 원문):
|
|
161
|
+
# agent_id: optional — "Subagent identifier. Present only when the hook fires from within a
|
|
162
|
+
# subagent (e.g., a tool called by an AgentTool worker). Absent for the main thread, even in
|
|
163
|
+
# --agent sessions. Use this field (not agent_type) to distinguish subagent calls from
|
|
164
|
+
# main-thread calls."
|
|
165
|
+
# 그리고 공통 페이로드 빌더가 모든 훅 이벤트에 `agent_id: o?.agentId`를 실어 보낸다.
|
|
166
|
+
# 즉 플랫폼이 명시적으로 지정한 판별 필드다. `agent_type`은 쓰지 않는다 — 같은 스키마가
|
|
167
|
+
# "--agent로 시작한 세션의 메인 스레드에도(agent_id 없이) 존재한다"고 못박고 있다.
|
|
168
|
+
#
|
|
169
|
+
# 다른 두 후보가 왜 작동하지 않는지(실측):
|
|
170
|
+
# * 트랜스크립트의 isSidechain / agentId 레코드 필드 — 서브에이전트 전용 파일
|
|
171
|
+
# agent-<hex>.jsonl의 전 레코드에 존재하지만(실측 65/65, 3/3), 공통 빌더는
|
|
172
|
+
# `transcript_path: Lp(e.id)`로 **세션 id**에서 경로를 만들고 서브에이전트는 부모의
|
|
173
|
+
# session id를 공유한다(실측: agent-*.jsonl의 sessionId == 부모 UUID). 따라서 서브에이전트
|
|
174
|
+
# 안에서 발화한 훅이 읽는 파일은 **부모의 메인 트랜스크립트**이고, 그 파일에는 isSidechain
|
|
175
|
+
# true도 agentId도 없다. 이것이 바로 잠식 경로다.
|
|
176
|
+
# * transcript_path의 `agent-*.jsonl` 경로 패턴 — 같은 이유로 그 경로가 오지 않는다
|
|
177
|
+
# (`agent_transcript_path`는 SubagentStop 페이로드에만 추가되는 별도 필드다).
|
|
178
|
+
#
|
|
179
|
+
# SubagentStart/SubagentStop은 예외다: 두 이벤트의 스키마는 agent_id를 **필수**로 두고 그 값은
|
|
180
|
+
# "대상 서브에이전트"의 식별자다(`Ae().and({hook_event_name:"SubagentStop", agent_id, ...})`).
|
|
181
|
+
# 즉 이 두 이벤트에서 agent_id는 "서브에이전트 안에서 발화했다"는 표지가 아니므로, 여기서
|
|
182
|
+
# 게이트를 걸면 #1545 자율 루프 커버리지가 통째로 사라진다.
|
|
183
|
+
case "$hook_event_name" in
|
|
184
|
+
SubagentStart|SubagentStop)
|
|
185
|
+
: # agent_id는 대상 식별자 — 판별 신호로 쓰지 않는다
|
|
186
|
+
;;
|
|
187
|
+
*)
|
|
188
|
+
if [ -n "$agent_id" ]; then
|
|
189
|
+
exit 0
|
|
190
|
+
fi
|
|
191
|
+
;;
|
|
192
|
+
esac
|
|
193
|
+
|
|
146
194
|
# hook_event_name이 없으면 hookSpecificOutput.hookEventName을 정확히 채울 수 없다.
|
|
147
195
|
# 잘못된 기본값은 출력을 무효화하므로 추측하지 않고 즉시 종료한다 (fallback 제거).
|
|
148
196
|
if [ -z "$hook_event_name" ]; then
|
|
@@ -221,9 +269,31 @@ split("\n")
|
|
|
221
269
|
else 1 end) as $r007
|
|
222
270
|
| ([ $blocks[] | select(.type? == "text") | (.text? // "") ] | join("\n") | split("\n")) as $lines
|
|
223
271
|
| ([ $lines[] | select(test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?Tool:")) ] | length) as $an_tool
|
|
272
|
+
# Core Rule 접두사 중 Agent 스폰을 가리키는 것만 따로 센다 — 아래 스폰 표기 중복 차감용.
|
|
273
|
+
| ([ $lines[] | select(test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?Tool: ?Agent")) ] | length) as $an_tool_agent
|
|
224
274
|
| ([ $lines[] | select(test("^[[:space:]]*\\[[0-9]+\\][[:space:]].*(→|->|—>)")) ] | length) as $an_spawn_item
|
|
275
|
+
# 단일 스폰 표기 (#1650 E): R008 §"Agent Tool Format"이 규정하는 `subagent_type:model → desc`
|
|
276
|
+
# 라인. 같은 절의 Parallel Spawn Prefix Rule은 **단일 스폰을 `[N]` 접두사에서 명시적으로
|
|
277
|
+
# 면제**하므로, 번호 없는 이 형태가 단일 스폰의 정규 표기다. $an_spawn_item은 번호 형태만
|
|
278
|
+
# 세므로 이 라인은 announce에 0을 기여했고, Agent tool_use가 그대로 위반으로 계상됐다.
|
|
279
|
+
# 줄 시작 앵커(선행 공백만 허용)는 필수다 — `- mgr-x:sonnet → ...` 같은 리스트 산문이나
|
|
280
|
+
# 문장 중간 인용이 announce로 계상되면 forward 판정(max(0, ntools - announce))에서 실제
|
|
281
|
+
# 누락을 가려버린다.
|
|
282
|
+
| ([ $lines[] | select(test("^[[:space:]]*[a-z][a-z0-9-]*:(haiku|sonnet|opus|fable|inherit)[[:space:]]*(→|->|—>)")) ] | length) as $an_spawn_single
|
|
225
283
|
| ([ $lines[] | select(test("\\[.+\\]\\[.+\\] ?(→|->|—>) ?Spawning:")) ] | length) as $an_spawn_hdr
|
|
226
|
-
|
|
284
|
+
# 스폰 표기의 단위는 세분화된 것 우선: 번호 라인 > 단일 스폰 라인 > Spawning 헤더.
|
|
285
|
+
| (if $an_spawn_item > 0 then $an_spawn_item
|
|
286
|
+
elif $an_spawn_single > 0 then $an_spawn_single
|
|
287
|
+
else $an_spawn_hdr end) as $an_spawn
|
|
288
|
+
# 중복 계수 차감 (#1650 E): 같은 턴에 Core Rule `→ Tool: Agent` 접두사가 함께 있으면 스폰
|
|
289
|
+
# 표기 라인은 **같은 호출의 동반 라인**이다 — `→ Target:`이 `→ Tool:`의 동반 라인이라 세지
|
|
290
|
+
# 않는 것과 동일한 논리다. 더해버리면 한 호출이 2로 계상돼, 같은 턴의 다른 진짜 누락을
|
|
291
|
+
# 가린다(forward는 max(0, ntools - announce)라 과대 announce는 조용한 마스킹이 된다).
|
|
292
|
+
# 그래서 스폰 기여분에서 `→ Tool: Agent` 라인 수를 빼되 0에서 바닥을 친다 — 빼기만 하므로
|
|
293
|
+
# announce가 늘지 않아 신규 오탐은 생기지 않는다. 대신 구식의 과대 announce(접두사 +
|
|
294
|
+
# 항목 이중 계수)가 가리던 진짜 누락 — 예: 접두사 1 + [1] 항목 1 + tools=(Agent, Read)에서
|
|
295
|
+
# 미announce Read — 은 새로 발화한다(의도된 동작, v1.1.62 적대적 리뷰 재현 C1/C2).
|
|
296
|
+
| ($an_tool + (if $an_spawn > $an_tool_agent then $an_spawn - $an_tool_agent else 0 end)) as $announce
|
|
227
297
|
| ([ $blocks[] | select((.type? == "tool_use") and ((.name? // "") != "Skill")) ] | length) as $ntools
|
|
228
298
|
# 전체 tool_use(Skill 포함)가 0건인 턴은 forward 판정을 무조건 0으로 고정한다(#1625
|
|
229
299
|
# 찐빠 #3, 이슈 제안 그대로). $ntools(Skill 제외) 기반 산식은 정상 입력에서는 이미
|
|
@@ -12,9 +12,14 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
12
12
|
|
|
13
13
|
input=$(cat)
|
|
14
14
|
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# Non-object stdin guard (#1650 B): a hook must never crash on garbage stdin.
|
|
16
|
+
# jq's parse error propagates through `set -euo pipefail` as rc=5 without this.
|
|
17
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
19
|
+
# Extract tool info — `?` suppresses errors when tool_input arrives as a scalar
|
|
20
|
+
tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name? // "unknown"')
|
|
21
|
+
tool_input=$(printf '%s\n' "$input" | jq -c 'if (.tool_input? | type) == "object" then .tool_input else {} end' 2>/dev/null) || tool_input='{}'
|
|
22
|
+
[ -n "$tool_input" ] || tool_input='{}'
|
|
18
23
|
|
|
19
24
|
SCHEMA_FILE=".claude/schemas/tool-inputs.json"
|
|
20
25
|
|
|
@@ -28,8 +33,8 @@ warnings=()
|
|
|
28
33
|
|
|
29
34
|
case "$tool_name" in
|
|
30
35
|
"Write")
|
|
31
|
-
file_path=$(printf '%s\n' "$tool_input" | jq -r '.file_path // ""')
|
|
32
|
-
content=$(printf '%s\n' "$tool_input" | jq -r '.content // ""')
|
|
36
|
+
file_path=$(printf '%s\n' "$tool_input" | jq -r '.file_path? // ""')
|
|
37
|
+
content=$(printf '%s\n' "$tool_input" | jq -r '.content? // ""')
|
|
33
38
|
|
|
34
39
|
if [ -z "$file_path" ]; then
|
|
35
40
|
warnings+=("[Schema] Write: file_path is empty or missing")
|
|
@@ -40,9 +45,9 @@ case "$tool_name" in
|
|
|
40
45
|
;;
|
|
41
46
|
|
|
42
47
|
"Edit")
|
|
43
|
-
file_path=$(printf '%s\n' "$tool_input" | jq -r '.file_path // ""')
|
|
44
|
-
old_string=$(printf '%s\n' "$tool_input" | jq -r '.old_string // ""')
|
|
45
|
-
new_string=$(printf '%s\n' "$tool_input" | jq -r '.new_string // ""')
|
|
48
|
+
file_path=$(printf '%s\n' "$tool_input" | jq -r '.file_path? // ""')
|
|
49
|
+
old_string=$(printf '%s\n' "$tool_input" | jq -r '.old_string? // ""')
|
|
50
|
+
new_string=$(printf '%s\n' "$tool_input" | jq -r '.new_string? // ""')
|
|
46
51
|
|
|
47
52
|
if [ -z "$file_path" ]; then
|
|
48
53
|
warnings+=("[Schema] Edit: file_path is empty or missing")
|
|
@@ -56,7 +61,7 @@ case "$tool_name" in
|
|
|
56
61
|
;;
|
|
57
62
|
|
|
58
63
|
"Bash")
|
|
59
|
-
command=$(printf '%s\n' "$tool_input" | jq -r '.command // ""')
|
|
64
|
+
command=$(printf '%s\n' "$tool_input" | jq -r '.command? // ""')
|
|
60
65
|
|
|
61
66
|
if [ -z "$command" ]; then
|
|
62
67
|
warnings+=("[Schema] Bash: command is empty")
|
|
@@ -12,9 +12,51 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
12
12
|
|
|
13
13
|
input=$(cat)
|
|
14
14
|
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# Non-object stdin guard (#1650 B) — rejects ONLY non-object stdin, which carries
|
|
16
|
+
# no scannable payload anyway; a well-formed object always reaches the scan below.
|
|
17
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
19
|
+
tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name? // "unknown"')
|
|
20
|
+
|
|
21
|
+
# Collect every text-bearing field of the payload into one scan buffer.
|
|
22
|
+
#
|
|
23
|
+
# PostToolUse carries the result under `tool_response`, NOT `tool_output`
|
|
24
|
+
# (measured 2026-09-03: 1764 PostToolUse payloads echoed into this project's
|
|
25
|
+
# session transcripts had `tool_response` 1764/1764 and `tool_output` 0/1764;
|
|
26
|
+
# the CC 2.1.259 embedded hook reference documents `"tool_response": {...}
|
|
27
|
+
# // PostToolUse only`). Reading `.tool_output.output` therefore always yielded
|
|
28
|
+
# "" and the scan below never ran — every AWS key, private key and PAT passed
|
|
29
|
+
# through unflagged.
|
|
30
|
+
#
|
|
31
|
+
# Measured text-bearing fields, by tool:
|
|
32
|
+
# Bash .tool_response.stdout / .stderr
|
|
33
|
+
# Read .tool_response.file.content
|
|
34
|
+
# Write .tool_response.content
|
|
35
|
+
# Agent .tool_response.prompt
|
|
36
|
+
# MCP .tool_response (string) or .tool_response.content[].text
|
|
37
|
+
# `.tool_output.output` and a string-shaped `.tool_output` are kept as
|
|
38
|
+
# fallbacks for events that still use the older shape (e.g. SubagentStop).
|
|
39
|
+
#
|
|
40
|
+
# `?` suppresses jq path errors when a field is a scalar rather than an object;
|
|
41
|
+
# every branch is optional, so an unexpected shape degrades to "" instead of
|
|
42
|
+
# aborting the hook under `set -euo pipefail`.
|
|
43
|
+
output=$(printf '%s\n' "$input" | jq -r '
|
|
44
|
+
[
|
|
45
|
+
(.tool_response? | if type == "string" then . else empty end),
|
|
46
|
+
(.tool_response?.stdout? | strings),
|
|
47
|
+
(.tool_response?.stderr? | strings),
|
|
48
|
+
(.tool_response?.content? | strings),
|
|
49
|
+
(.tool_response?.content? | if type == "array"
|
|
50
|
+
then map(.text? | strings) | join("\n")
|
|
51
|
+
else empty end),
|
|
52
|
+
(.tool_response?.file?.content? | strings),
|
|
53
|
+
(.tool_response?.prompt? | strings),
|
|
54
|
+
(.tool_response?.output? | strings),
|
|
55
|
+
(.tool_output? | if type == "string" then . else empty end),
|
|
56
|
+
(.tool_output?.output? | strings)
|
|
57
|
+
]
|
|
58
|
+
| map(select(. != "")) | join("\n")
|
|
59
|
+
' 2>/dev/null) || output=""
|
|
18
60
|
|
|
19
61
|
# Skip if no output
|
|
20
62
|
if [ -z "$output" ] || [ "$output" = "null" ]; then
|
|
@@ -44,7 +86,7 @@ if echo "$output" | grep -qE 'ghp_[a-zA-Z0-9]{36}'; then
|
|
|
44
86
|
fi
|
|
45
87
|
|
|
46
88
|
# Private Key
|
|
47
|
-
if echo "$output" | grep -qE '-----BEGIN.*PRIVATE KEY-----'; then
|
|
89
|
+
if echo "$output" | grep -qE -- '-----BEGIN.*PRIVATE KEY-----'; then
|
|
48
90
|
echo "[Security] Potential private key detected in ${tool_name} output" >&2
|
|
49
91
|
detected=true
|
|
50
92
|
fi
|
|
@@ -13,6 +13,10 @@ set -euo pipefail
|
|
|
13
13
|
# ── Stop hook 프로토콜: stdin을 먼저 읽음 ──
|
|
14
14
|
input=$(cat)
|
|
15
15
|
|
|
16
|
+
# ── 비객체 stdin 가드 (#1650 B) — 없으면 jq 파싱 오류가 pipefail로 rc=5 전파.
|
|
17
|
+
# worker의 `.message.role` 트랜스크립트 파서 경로는 그대로 유지된다.
|
|
18
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
19
|
+
|
|
16
20
|
# ── Opt-out 체크 ──
|
|
17
21
|
if [ "${OMCUSTOM_SESSION_REFLECTION:-}" = "off" ]; then
|
|
18
22
|
printf '%s\n' "$input"
|
|
@@ -40,7 +44,7 @@ bg_tasks_count=$(printf '%s\n' "$bg_tasks_json" | jq 'length' 2>/dev/null || ech
|
|
|
40
44
|
session_crons_count=$(printf '%s\n' "$session_crons_json" | jq 'length' 2>/dev/null || echo 0)
|
|
41
45
|
|
|
42
46
|
# 미완료 백그라운드 태스크: running/pending/in_progress 상태 카운트
|
|
43
|
-
bg_dangling_count=$(printf '%s\n' "$bg_tasks_json" | jq '[.[] | select(.status == "running" or .status == "pending" or .status == "in_progress")] | length' 2>/dev/null || echo 0)
|
|
47
|
+
bg_dangling_count=$(printf '%s\n' "$bg_tasks_json" | jq '[.[] | select(.status? == "running" or .status? == "pending" or .status? == "in_progress")] | length' 2>/dev/null || echo 0)
|
|
44
48
|
|
|
45
49
|
# ── 경로 결정 (환경변수 override 지원) ──
|
|
46
50
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -204,17 +204,34 @@ _strip_heredoc_bodies() {
|
|
|
204
204
|
local text="$1"
|
|
205
205
|
local out="" line tok delim="" in_body=0
|
|
206
206
|
local before_op dq sq
|
|
207
|
+
# A literal single quote cannot be written inside the bracket expression of
|
|
208
|
+
# the quote-counting expansion below, so hold one in a variable.
|
|
209
|
+
local sq_char="'"
|
|
207
210
|
while IFS= read -r line; do
|
|
208
211
|
if [ "$in_body" -eq 1 ]; then
|
|
209
212
|
# "<<-" allows leading tabs before the terminator; accept leading
|
|
210
|
-
# whitespace for either form.
|
|
211
|
-
|
|
213
|
+
# whitespace for either form. The trim is pure parameter expansion: as
|
|
214
|
+
# "printf | sed" it cost TWO forks on every BODY line, which is the
|
|
215
|
+
# dominant cost of a long heredoc (measured: a 1000-line body took
|
|
216
|
+
# 1.8 s of hook latency, #1650 A).
|
|
217
|
+
tok="${line#"${line%%[![:space:]]*}"}"
|
|
218
|
+
tok="${tok%"${tok##*[![:space:]]}"}"
|
|
212
219
|
if [ "$tok" = "$delim" ]; then
|
|
213
220
|
in_body=0
|
|
214
221
|
fi
|
|
215
222
|
continue
|
|
216
223
|
fi
|
|
217
224
|
out="${out}${line}"$'\n'
|
|
225
|
+
# A line with no "<<" can open no heredoc, so neither the forgery guard nor
|
|
226
|
+
# the delimiter scan below has anything to find on it. Skipping them keeps
|
|
227
|
+
# the extraction "sed" off every ordinary line, which used to run
|
|
228
|
+
# unconditionally — two more forks per line (#1650 A). "delim" must still be
|
|
229
|
+
# cleared here, or a delimiter captured on an EARLIER line would re-open a
|
|
230
|
+
# body on this one.
|
|
231
|
+
case "$line" in
|
|
232
|
+
*'<<'*) ;;
|
|
233
|
+
*) delim=""; continue ;;
|
|
234
|
+
esac
|
|
218
235
|
# A DELIMITER FORGERY guard: the extraction below has no idea whether the
|
|
219
236
|
# "<<" it finds is shell syntax or literal text inside a quoted string, so
|
|
220
237
|
# `echo "a << EOF"` followed by `rm -rf x` and a line reading `EOF` used to
|
|
@@ -226,14 +243,15 @@ _strip_heredoc_bodies() {
|
|
|
226
243
|
# as a write. "${line%<<*}" strips the shortest matching suffix, i.e. it
|
|
227
244
|
# yields the text before the LAST "<<" — the same occurrence the greedy
|
|
228
245
|
# extraction regex below settles on.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
246
|
+
# The quote counts are parameter expansions rather than
|
|
247
|
+
# "printf | tr | wc | tr" (four more forks per opener line): "${v//[^X]/}"
|
|
248
|
+
# deletes everything that is not X, so the remaining LENGTH is the count.
|
|
249
|
+
before_op="${line%<<*}"
|
|
250
|
+
dq="${before_op//[^\"]/}"
|
|
251
|
+
sq="${before_op//[^$sq_char]/}"
|
|
252
|
+
if [ $(( ${#dq} % 2 )) -eq 1 ] || [ $(( ${#sq} % 2 )) -eq 1 ]; then
|
|
253
|
+
printf '%s' "${out}__QUOTED_HEREDOC_OPENER__"$'\n'
|
|
254
|
+
return 0
|
|
237
255
|
fi
|
|
238
256
|
delim="$(printf '%s' "$line" \
|
|
239
257
|
| sed -n -E "s/^.*[^<]<<-?[[:space:]]*[\"']?([A-Za-z_][A-Za-z0-9_]*)[\"']?.*\$/\\1/p")"
|
|
@@ -453,7 +471,12 @@ is_readonly_bash_command() {
|
|
|
453
471
|
normalized="${normalized//|/$'\n'}"
|
|
454
472
|
local seg
|
|
455
473
|
while IFS= read -r seg; do
|
|
456
|
-
|
|
474
|
+
# Pure parameter expansion, not "printf | sed": each segment is a single
|
|
475
|
+
# line, so the two forms trim identically, but the sed form spent two
|
|
476
|
+
# forks on EVERY segment of a multi-line command (measured: 300
|
|
477
|
+
# newline-separated segments took 0.8 s of hook latency, #1650 A).
|
|
478
|
+
seg="${seg#"${seg%%[![:space:]]*}"}"
|
|
479
|
+
seg="${seg%"${seg##*[![:space:]]}"}"
|
|
457
480
|
if [ -z "$seg" ]; then
|
|
458
481
|
echo "false"
|
|
459
482
|
return 0
|
|
@@ -472,6 +495,15 @@ is_readonly_bash_command() {
|
|
|
472
495
|
|
|
473
496
|
input=$(cat)
|
|
474
497
|
|
|
498
|
+
# A hook must never crash the tool call it observes. stdin that is not a JSON
|
|
499
|
+
# OBJECT — empty, a bare scalar, a JSON array, truncated, or not JSON at all —
|
|
500
|
+
# carries no tool record, and the very first "jq -r" below died on it; with
|
|
501
|
+
# "set -euo pipefail" that took the WHOLE hook down with rc=5 (#1650 B).
|
|
502
|
+
# Swallow it instead: exit 0 with no stdout, exactly as if the hook had not
|
|
503
|
+
# run. "jq -e" exits non-zero both for a parse error and for a false/null
|
|
504
|
+
# result, so one guard covers every non-object shape.
|
|
505
|
+
printf '%s' "$input" | jq -e 'type == "object"' >/dev/null 2>&1 || exit 0
|
|
506
|
+
|
|
475
507
|
# Extract tool info
|
|
476
508
|
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
|
|
477
509
|
# Bash commands are NOT files. Feeding tool_input.command into file_path made
|
|
@@ -11,28 +11,35 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
11
11
|
|
|
12
12
|
input=$(cat)
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
# Non-object stdin guard (#1650 B): a hook must never crash on garbage stdin.
|
|
15
|
+
# jq's parse error propagates through `set -euo pipefail` as rc=5 without this.
|
|
16
|
+
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
17
|
+
|
|
18
|
+
# Extract task info — support both PostToolUse (tool_input.*) and SubagentStop (top-level) shapes.
|
|
19
|
+
# `?` suppresses "Cannot index string with ..." when tool_input arrives as a scalar.
|
|
20
|
+
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // .agent_type? // "unknown"')
|
|
21
|
+
model=$(printf '%s\n' "$input" | jq -r '.tool_input.model? // .model? // "inherit"')
|
|
22
|
+
description=$(printf '%s\n' "$input" | jq -r '.tool_input.description? // .description? // ""' | head -c 80)
|
|
18
23
|
|
|
19
24
|
# Extract skill name from description or prompt
|
|
20
25
|
skill_name=""
|
|
26
|
+
# `|| true`: a no-match grep exits 1 and `set -euo pipefail` would abort the hook
|
|
27
|
+
# (measured rc=1 on empty stdin AND on a well-formed object with no skill name).
|
|
21
28
|
if echo "$description" | grep -qiE '(skill:|routing|→.*skill)'; then
|
|
22
|
-
skill_name=$(echo "$description" | grep -oiE '[a-z]+-[a-z]+(-[a-z]+)*-?(routing|skill|practices|detection|decomposition|orchestration|pipeline|guards|cycle|plan|review|refactor|publish|version|audit|exec|analyze|bundle|report|setup|watch|lists|status|help|save|recall)' | head -1)
|
|
29
|
+
skill_name=$(echo "$description" | grep -oiE '[a-z]+-[a-z]+(-[a-z]+)*-?(routing|skill|practices|detection|decomposition|orchestration|pipeline|guards|cycle|plan|review|refactor|publish|version|audit|exec|analyze|bundle|report|setup|watch|lists|status|help|save|recall)' | head -1 || true)
|
|
23
30
|
fi
|
|
24
31
|
# Fallback: check prompt field for "Skill: {name}" pattern
|
|
25
32
|
if [ -z "$skill_name" ]; then
|
|
26
|
-
prompt=$(printf '%s\n' "$input" | jq -r '.tool_input.prompt // ""' | head -c 500)
|
|
27
|
-
skill_name=$(echo "$prompt" | grep -oiE 'Skill:\s*[a-z]+-[a-z]+(-[a-z]+)*' | sed 's/[Ss]kill:\s*//' | head -1)
|
|
33
|
+
prompt=$(printf '%s\n' "$input" | jq -r '.tool_input.prompt? // ""' | head -c 500)
|
|
34
|
+
skill_name=$(echo "$prompt" | grep -oiE 'Skill:\s*[a-z]+-[a-z]+(-[a-z]+)*' | sed 's/[Ss]kill:\s*//' | head -1 || true)
|
|
28
35
|
fi
|
|
29
36
|
|
|
30
37
|
# Determine outcome
|
|
31
|
-
is_error=$(printf '%s\n' "$input" | jq -r '.tool_output.is_error // false')
|
|
38
|
+
is_error=$(printf '%s\n' "$input" | jq -r '.tool_output.is_error? // false')
|
|
32
39
|
|
|
33
40
|
if [ "$is_error" = "true" ]; then
|
|
34
41
|
outcome="failure"
|
|
35
|
-
error_summary=$(printf '%s\n' "$input" | jq -r '.tool_output.output // ""' | head -c 200)
|
|
42
|
+
error_summary=$(printf '%s\n' "$input" | jq -r '.tool_output.output? // ""' | head -c 200)
|
|
36
43
|
else
|
|
37
44
|
outcome="success"
|
|
38
45
|
error_summary=""
|
|
@@ -271,10 +271,14 @@ This is the interrupt-intent extension of Read-Before-Characterize ("actual inte
|
|
|
271
271
|
| 에러 원인 추정 | 첫 가설로 워크플로우/설정 영구 수정 | 가설을 좁은 범위에서 검증 후 변경 |
|
|
272
272
|
| CI/publish 실패 | 추정 기반 우회 커밋 머지 | 에러 메시지/로그로 실제 원인 확정 |
|
|
273
273
|
| 권한/토큰 오류 | 플래그/옵션 변경으로 우회 시도 | 권한 범위·토큰 종류 직접 확인 |
|
|
274
|
+
| 트랜스크립트·로그 통계로 원인 추정 | 통계적 상관(예: "첫 레코드 thinking 13/21")만으로 코드 경로의 인과를 "확정"해 이슈 코멘트·메모리에 기록 | 해당 코드 경로를 읽고 판정 로직을 1:1 재현(jq/스크립트)해 인과를 확인한 뒤 기록; 그 전에는 R011 `[hypothesis]` 태그로만 저장 |
|
|
274
275
|
|
|
275
276
|
### Common Violation (#1217 item #4)
|
|
276
277
|
npm publish E403을 `--provenance` attestation 충돌로 오진단 → release workflow에서 `--provenance` 제거 커밋 머지 → 2차 시도 동일 실패 → 실제 원인은 NPM_TOKEN 권한(Automation token 필요). 잘못된 추정으로 릴리즈 워크플로우를 영구 변경.
|
|
277
278
|
|
|
279
|
+
### Common Violation (#1652 #1) — 통계적 상관 ≠ 코드 인과
|
|
280
|
+
v1.1.59 세션에서 #1643(advisor "R007 헤더=0" 오탐) 원인을 트랜스크립트 통계만으로 "advisor가 thinking 전용 첫 레코드를 병합하지 못함"이라 이슈 코멘트·feedback 메모리에 확정 기록 → triage가 현행 코드(이미 병합·thinking 제외)와의 불일치를 지적 → 진단 에이전트가 jq로 판정 로직을 1:1 재현해 반박 — 실제 원인은 레이블 문구 모호성(값은 위반 건수인데 "헤더=0"으로 읽힘). 같은 세션 2회째: `gh pr merge --delete-branch` 로컬 부수효과를 reflog 3건으로 "확정" 저장했으나 4번째 머지(PR #1651)에서 재현되지 않아 "원인 미확정"으로 정정. 두 건 모두 상관을 인과로 승격한 Read-Before-Characterize 자기 위반이며, R011 즉시 저장과 결합해 틀린 전제가 영속화될 뻔했다. Cross-ref: R011 「Mid-Session Immediate Save」 `[hypothesis]` 행, R010 「저장소 상태 기재도 같은 규율」, 아래 「Self-Violation Counting Is Also Diagnosis」.
|
|
281
|
+
|
|
278
282
|
### Self-Check (영구 변경 전)
|
|
279
283
|
1. 가설을 뒷받침하는 직접 증거(로그/에러 코드/문서)가 있는가?
|
|
280
284
|
2. 비파괴적 방법으로 가설을 검증할 수 있는가?
|
|
@@ -16,7 +16,7 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
|
|
|
16
16
|
| Soft Block | Stop hook prompt | R011 session-end saves | Auto-performs then approves |
|
|
17
17
|
| Conversation Block | PostToolUse hook + `continueOnBlock` (CC v2.1.139+), exit 2 | stuck-detector, context-budget-advisor, cost-cap-advisor | Feeds rejection reason into conversation; Claude continues with awareness |
|
|
18
18
|
| Advisory | PostToolUse hooks | R007, R008, R009, R010, R018 | Warns via stderr, never blocks |
|
|
19
|
-
| Advisory (proactive) | UserPromptSubmit + SubagentStop + PostToolUse hooks | R007, R008 (`r007-r008-drift-advisor.sh` — #1229 UserPromptSubmit, #1545 SubagentStop, #1553 PostToolUse) | Reads last assistant turn; emits advisory if header/prefix absent. SubagentStop wiring (#1545) closes the no-user-input autonomous-loop gap (`/fsd`); PostToolUse (#1553) covers the orchestrator-only stretch before the first subagent spawn. Complements retroactive Stop-hook (`session-reflection.sh`, #1190). **v1.1.43부터 실제 발화 — 아래 각주 참조.** v1.1.49부터 역방향(announce > tool_use) 신호 포함, 기본 off 옵트인 (#1595 #6). |
|
|
19
|
+
| Advisory (proactive) | UserPromptSubmit + SubagentStop + PostToolUse hooks | R007, R008 (`r007-r008-drift-advisor.sh` — #1229 UserPromptSubmit, #1545 SubagentStop, #1553 PostToolUse) | Reads last assistant turn; emits advisory if header/prefix absent. SubagentStop wiring (#1545) closes the no-user-input autonomous-loop gap (`/fsd`); PostToolUse (#1553) covers the orchestrator-only stretch before the first subagent spawn. Complements retroactive Stop-hook (`session-reflection.sh`, #1190). **v1.1.43부터 실제 발화 — 아래 각주 참조.** v1.1.49부터 역방향(announce > tool_use) 신호 포함, 기본 off 옵트인 (#1595 #6). **v1.1.62(#1650 D)부터 서브에이전트 세션에서는 침묵**: hook stdin의 `agent_id`(CC 스키마상 서브에이전트 내부 발화에만 존재)가 있으면 exit 0 — 단 `SubagentStart`/`SubagentStop`은 `agent_id`가 대상 식별자라 예외(판정 수행). 중첩 서브에이전트 안의 SubagentStop은 억제 못 함(R010 정책상 도달 불가 경로). 완료 보고가 advisory 응답으로 대체되던 훅 피드백 잠식(R020 8항, #1652 #3)의 advisory 계열 차단. |
|
|
20
20
|
| Advisory (telemetry) | PostToolUseFailure hook | — (계측 전용, 규칙 강제 없음) | `failure-ledger.sh` (#1561, v1.1.44) — 도구 실패를 JSONL 원장에 append. stdout/stderr 무출력이라 모델에 도달하지 않으며 절대 차단하지 않음 |
|
|
21
21
|
| Advisory (proactive) | UserPromptSubmit hook | R020 (원인 진단) | `fail-axis-cause-advisor.sh` (#1561, v1.1.44) — 원장에 실패 기록이 있는데 원인 진술 없는 재촉 프롬프트가 오면 `hookSpecificOutput.additionalContext`로 "원인 가설 되묻기" advisory 전달. 원장 부재 시 조용히 통과 |
|
|
22
22
|
| Prompt-based | CLAUDE.md + rules/ + `SessionStart` 재주입(matcher `*`; PostCompact 이벤트 배선은 유지되나 효과 미보장[^postcompact]) | All MUST rules | Behavioral guidance in context |
|
|
@@ -690,6 +690,8 @@ Subagent NOT required for:
|
|
|
690
690
|
|
|
691
691
|
"Simple" means READ-ONLY operations. If the task involves any file creation, modification, or deletion, it must be delegated. There is no "too small to delegate" exception for write operations.
|
|
692
692
|
|
|
693
|
+
**Carve-out — PPID 스코프 `/tmp` 런타임 상태 마커 (Origin: #1650 C, v1.1.61)**: 프로젝트 트리 **밖**의 `/tmp` 아래에 세션 프로세스 ID로 스코프된 1줄짜리 런타임 마커(예: `/tmp/.claude-fsd-$PPID`)를 만들거나 지우는 것은 프로젝트 파일 쓰기가 아니므로 오케스트레이터가 Bash 한 줄로 직접 수행할 수 있다. 조건: (1) 경로가 `/tmp` 하위이고 프로세스 ID로 스코프됨, (2) 내용이 타임스탬프 등 1줄 신호에 불과함, (3) 프로젝트 트리·홈 디렉토리·설정 파일을 건드리지 않음. 구조화된 파이프라인 상태(`/tmp/.claude-pipeline-{name}-{PPID}.json`)는 이 carve-out 대상이 아니며 기존대로 `tracker-checkpoint`에 위임한다 — 마커와 상태 파일의 차이는 내용 구조(1줄 신호 vs 검증이 필요한 JSON 상태)다. 위임 비용(스폰 30초 이상)이 작업(1줄 touch)보다 큰 경우를 위한 좁은 예외이며, "작아서" 예외가 아니라 **프로젝트 파일이 아니라서** 예외다.
|
|
694
|
+
|
|
693
695
|
## Dynamic Agent Creation (No-Match Fallback)
|
|
694
696
|
|
|
695
697
|
When routing detects no matching agent for a specialized task:
|
|
@@ -131,6 +131,10 @@ Origin: #1521 (찐빠 #2 — v1.1.32 skills 118→114 동기화에서 파일 열
|
|
|
131
131
|
|
|
132
132
|
Any change to: agents, agent frontmatter, skills, guides, routing patterns, rules, wiki pages.
|
|
133
133
|
|
|
134
|
+
### auto-dev.yaml 압축 티어와의 양방향 참조 (Origin: #1650 C 부수)
|
|
135
|
+
|
|
136
|
+
이 소절은 R017 적용 범위 자체를 넓히지 않는다 — 위 범위 열거가 압축 파이프라인에서 어떻게 배선되는지를 기록한다. `.claude/skills/pipeline/workflows/auto-dev.yaml`의 compression-mode-eval은 docs-only 티어에서 deep-verify를 self-review로 대체하되, 변경 집합에 `.claude/rules/**`(또는 agents/skills frontmatter 등 구조 표면)가 포함되면 mgr-sauron R017 검증을 단일 목표 위임으로 **필수 실행**하는 carve-out을 둔다(v1.1.58 세션 — docs-only 티어였으나 sauron이 R002/R010 모순 advisory 1건을 포착). 이 절의 적용 조건과 그 carve-out은 같은 조건을 서로 가리킨다 — 한쪽을 바꾸면 다른 쪽도 같은 커밋에서 갱신한다(R016 Rule Wiring Check). 이전까지는 yaml → R017 단방향 인용만 있어 R017 쪽에서 carve-out의 존재를 알 수 없었다.
|
|
137
|
+
|
|
134
138
|
## Structural Migration Verification
|
|
135
139
|
|
|
136
140
|
디렉토리 재구조화, 템플릿 평탄화(flat templates), 브랜치 전략 변경 등 **구조 마이그레이션** 시, 경로 참조와 파일 존재성 회귀를 사전 검사해야 한다. 표준 5-round 검증이 콘텐츠 정합성에 집중하는 반면, 구조 마이그레이션은 경로·존재성 회귀를 별도로 점검한다.
|
|
@@ -78,6 +78,17 @@ Agent(description: "[2] Python code review", subagent_type: "lang-python-expert"
|
|
|
78
78
|
|
|
79
79
|
Single agent spawns do NOT use the `[N]` prefix.
|
|
80
80
|
|
|
81
|
+
**단일 스폰도 Core Rule 접두사 필수 (Origin: #1652 #3-3, advisor 정합 #1650 E)**: 단일 Agent 스폰은 `[N]` 항목 형식 대신 Core Rule 형식의 접두사 라인(에이전트·모델 대괄호 + 화살표 + Tool 표기 + `Agent`)을 호출 직전에 출력한다. advisor(`.claude/hooks/scripts/r007-r008-drift-advisor.sh`)가 announce로 계수하는 것은 4종 — Tool 표기 라인, 대괄호 번호가 붙은 spawn 항목, **대괄호 번호 없이 에이전트타입:모델 다음에 화살표와 설명이 이어지는 단독 라인**(#1650 E에서 추가), Spawning 헤더 라인 — 이며 스폰 표기 3종은 **번호 항목 > 단독 라인 > Spawning 헤더** 순으로 하나만 채택된다. 단독 라인만 쓴 응답이 R008 누락으로 오계상되던 결함은 v1.1.62에서 해소됐다(v1.1.60 세션 실측 → v1.1.61 적대적 리뷰 재현 → #1650 E 적용). **다만 규칙이 요구하는 형식은 여전히 Core Rule 접두사 라인이다** — 단독 라인 계수는 오탐 제거이지 표기 승격이 아니다. 접두사 라인과 단독 라인이 함께 있으면 advisor는 단독 라인을 **동반 라인으로 보아 차감**하므로(Target 라인과 동일 취급) 병기해도 이중 계수되지 않는다. **단독 라인은 반드시 줄 시작(선행 공백만 허용)이어야 한다** — 리스트 마커가 앞에 붙으면 계수되지 않는다. advisor 판정식을 바꿀 때 이 문단을 같은 커밋에서 갱신한다(R016 Rule Wiring Check).
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
[claude][opus] → Tool: Agent
|
|
85
|
+
[claude][opus] → Target: mgr-updater:sonnet → R008 clause update
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| Anti-pattern | Required |
|
|
89
|
+
|--------------|----------|
|
|
90
|
+
| 단일 스폰을 번호 없는 에이전트타입:모델 화살표 설명 단독 라인으로만 announce | Core Rule 접두사 라인(Tool 표기 + Agent)을 호출 직전에 출력; 항목 라인은 Target 라인으로 병기 가능 |
|
|
91
|
+
|
|
81
92
|
This ensures the Running display:
|
|
82
93
|
```
|
|
83
94
|
⏺ Running 2 agents… (ctrl+o to expand)
|
|
@@ -358,6 +358,9 @@ Save memory IMMEDIATELY upon surprising discovery — do not defer to session en
|
|
|
358
358
|
| Unexpected tool behavior / workaround | Save `feedback_*.md` now | Session state defense |
|
|
359
359
|
| Subagent false-positive detected | Save `feedback_*.md` now | Prevent repeat in same session |
|
|
360
360
|
| User correction / feedback | Save `feedback_*.md` now | Honor correction immediately |
|
|
361
|
+
| Root-cause hypothesis (원인 진단) | Save `feedback_*.md` ONLY with a `[hypothesis: <unverified-basis summary>]` first-line tag until the code path has been read and the decision logic reproduced 1:1; promote to plain fact only after that verification | Statistical correlation from transcript counts is NOT code causation — an unverified cause saved as fact propagates to issues and future sessions (#1652 #1; R020 「통계적 상관 ≠ 코드 인과」) |
|
|
362
|
+
|
|
363
|
+
**`[hypothesis: …]` 태그 규약 (Origin: #1652 #1)**: 원인 진단을 메모리에 즉시 저장할 때는 본문 첫 줄에 `[hypothesis: <미검증 근거 요약>]`를 붙인다. 검증(코드 경로 대조·판정 로직 1:1 재현) 완료 시 태그를 제거하거나 파일을 정정한다. 태그가 남아 있는 항목은 후속 세션에서 전제로 쓰지 않고 **검증 트리거로만** 사용한다 — 위 「Safety-Related Feedback Memory Framing」과 같은 원리(결론형이 아니라 검증 의무형)다. 실증: v1.1.59 세션에서 #1643 원인을 트랜스크립트 통계(첫 레코드 thinking 13/21)만으로 "advisor가 thinking 전용 첫 레코드를 병합하지 못함"이라 확정 저장했다가 jq 1:1 재현으로 반박됐고(실제 원인은 레이블 문구 모호성), 같은 세션에서 `gh pr merge --delete-branch` 로컬 부수효과도 reflog 3건으로 "확정" 저장한 뒤 4번째 머지에서 재현되지 않아 "원인 미확정"으로 정정했다. 리터럴은 `[hypothesis:` 접두 하나로 통일하며, 검증 여부 조회는 `grep -l '^\[hypothesis:' <memory dir>`로 결정론적으로 수행한다. 최초 적용 대상: `feedback_gh_merge_delete_branch_local_side_effect.md`(v1.1.60 세션에서 "원인 미확정"으로 정정된 항목) — v1.1.61 세션 종료 시 sys-memory-keeper가 태그를 부착한다.
|
|
361
364
|
|
|
362
365
|
See rationale and cross-references via Read tool.
|
|
363
366
|
|
|
@@ -76,10 +76,37 @@ path pre-check(`auto-dev.yaml`)가 `.claude/hooks/**` 대상 이슈를 발견하
|
|
|
76
76
|
즉시 승인을 요청하지 않는다. 대신 그 이슈를 이번 스코프에서 **분리·이월**하고 나머지 적격 이슈로
|
|
77
77
|
계속 진행하며, `/homework` 반복 경계(게이트)에서 이월된 hooks 이슈들을 **1회 묶어** 승인 질문으로
|
|
78
78
|
제시한다. 승인을 받으면 R015 directive persistence로 세션 내 동일 카테고리에 지속 적용한다.
|
|
79
|
+
무인 여부의 판별 신호는 아래 「무인 모드 마커」 절이 정의한다.
|
|
80
|
+
|
|
81
|
+
### 무인 모드 마커 — 결정론적 판별 신호 (#1650 C)
|
|
82
|
+
|
|
83
|
+
`pipeline auto-dev`의 scope-selection Step 3가 요구하는 "무인 여부"는 산문 추론이 아니라 마커 파일로
|
|
84
|
+
판정한다. FSD 진입 시 오케스트레이터가 Bash로 아래 한 줄을 실행한다. 이 마커는 R010 「Exception:
|
|
85
|
+
Simple Tasks」의 carve-out(PPID 스코프 `/tmp` 런타임 상태 마커 — v1.1.61 신설)에 해당하므로
|
|
86
|
+
오케스트레이터가 직접 실행한다. 구조화된 파이프라인 상태(`/tmp/.claude-pipeline-*-{PPID}.json`)는 이
|
|
87
|
+
예외 대상이 아니며 기존대로 `tracker-checkpoint`에 위임된다.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
printf '%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "/tmp/.claude-fsd-$PPID"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- `$PPID`는 Bash 도구 셸의 부모, 즉 Claude Code 프로세스이며 호출 간 안정적이다(2026-09-03 실측: 두
|
|
94
|
+
호출 모두 동일 값).
|
|
95
|
+
- `claude -p`·예약 실행처럼 마커를 미리 만들 수 없는 경로는 env `OMCUSTOM_UNATTENDED=1`로 대체한다.
|
|
96
|
+
- `pipeline auto-dev` pre-triage Phase 0.6이 마커 존재 또는 env 값을 실측해 `unattended_mode` 상태를
|
|
97
|
+
산출하고, scope-selection Step 3는 그 상태로 `.claude/hooks/**` 이슈 분리·이월을 결정한다.
|
|
98
|
+
- 매 반복 시작 시 같은 명령으로 마커를 다시 써서 mtime을 갱신한다. Phase 0.6은 mtime이 6시간(360분)보다
|
|
99
|
+
오래된 마커를 **부재로 취급**한다 — 프로세스 크래시·PID 재사용으로 남은 마커가 이후 수동
|
|
100
|
+
`/pipeline auto-dev`를 무인으로 오판하지 않게 하는 가드다.
|
|
101
|
+
- 수렴(`[FSD Done]`), 릴리즈 cap 도달, safety classifier 차단으로 인한 중단, 사용자 인터럽트 — **어느
|
|
102
|
+
경로로 끝나든** `command rm -f "/tmp/.claude-fsd-$PPID"`로 마커를 제거한다 — 남겨두면
|
|
103
|
+
같은 세션의 이후 수동 `/pipeline auto-dev`가 무인으로 오판된다. `rm`이 alias(`trash`)로 셰이딩된
|
|
104
|
+
셸에서 `rm -f`는 거부되므로 `command`를 붙인다(R005, 2026-09-03 실측).
|
|
79
105
|
|
|
80
106
|
### Iteration Flow (per iteration)
|
|
81
107
|
|
|
82
108
|
```
|
|
109
|
+
[FSD Entry] write marker /tmp/.claude-fsd-$PPID
|
|
83
110
|
[FSD Iteration N]
|
|
84
111
|
├── /pipeline auto-dev → one release (PR create → merge → npm publish → milestone close)
|
|
85
112
|
├── /homework → extract 찐빠, confirm gate (or --dry-run if requested)
|
|
@@ -87,8 +114,9 @@ path pre-check(`auto-dev.yaml`)가 `.claude/hooks/**` 대상 이슈를 발견하
|
|
|
87
114
|
│ fix-then-merge (known CI failure pattern)
|
|
88
115
|
│ defer+surface (design decision needed)
|
|
89
116
|
└── Check convergence: eligible issues = 0 AND open PRs = 0 (merged or deferred)?
|
|
90
|
-
├── YES → [FSD Done] converged naturally
|
|
91
|
-
└── NO →
|
|
117
|
+
├── YES → [FSD Done] converged naturally → command rm -f /tmp/.claude-fsd-$PPID
|
|
118
|
+
└── NO → cap reached or classifier block? → YES → [FSD Stop] → command rm -f /tmp/.claude-fsd-$PPID
|
|
119
|
+
→ NO → next iteration (re-write marker first)
|
|
92
120
|
```
|
|
93
121
|
|
|
94
122
|
## Safety and Discipline
|
|
@@ -106,6 +134,16 @@ Each iteration operates under full project rules — no relaxation because FSD i
|
|
|
106
134
|
|
|
107
135
|
`/homework` runs as a **retrospective gate** between iterations — findings go through `omcustom-feedback`'s Phase 4A confirmation gate. The loop does NOT skip homework on the grounds that it is "automated". If homework requires user confirmation (e.g., to file a feedback issue), the loop pauses and waits.
|
|
108
136
|
|
|
137
|
+
**Homework 제출 게이트 묶음 (opt-in, #1652 #4)**: 기본 계약은 위와 같이 **반복마다** 게이트를
|
|
138
|
+
제시하는 것이다. 사용자가 세션 중 명시적으로 지시하거나(예: "homework 게이트는 수렴 시점에 1회로
|
|
139
|
+
묶어라") 무인 실행을 선언하며 묶음을 지정한 경우에만, homework 아티팩트(`homework-{HHmmss}.md`)는
|
|
140
|
+
반복마다 기록하되 `omcustom-feedback` Phase 4A 제출 게이트는 수렴 시점(또는 릴리즈 cap 도달 시점)에
|
|
141
|
+
**1회** 묶어 제시한다 — 이월된 `.claude/hooks/**` 승인 질문과 같은 자리에서 함께 제시한다. 묶음
|
|
142
|
+
여부는 사용자 결정 사항이며 오케스트레이터가 단독으로 선택하지 않는다(v1.1.59/60 세션에서
|
|
143
|
+
오케스트레이터가 단독 결정한 것이 스킬 계약 이탈로 회고됐다). 한 번 지시되면 R015 directive
|
|
144
|
+
persistence로 세션 내 유지된다. 묶음 여부는 `unattended_mode`(마커·env)에서 **도출하지 않는다** —
|
|
145
|
+
무인 판별은 hooks 이슈 분리·이월에만 쓰이고, 게이트 묶음은 사용자 지시가 유일한 트리거다.
|
|
146
|
+
|
|
109
147
|
If a release operation triggers the safety classifier, the current iteration stops and surfaces the block to the user before continuing.
|
|
110
148
|
|
|
111
149
|
## When to Use
|
|
@@ -78,6 +78,31 @@ steps:
|
|
|
78
78
|
4. Do NOT read, echo, or modify any credential material while checking settings — read the
|
|
79
79
|
single `permissions.defaultMode` field only (R001).
|
|
80
80
|
|
|
81
|
+
Phase 0.6 — Detect unattended mode deterministically (#1650 C).
|
|
82
|
+
|
|
83
|
+
Why: scope-selection Step 3 defers `.claude/hooks/**` issues in unattended runs. Until v1.1.60 the
|
|
84
|
+
signal was prose ("entered via /fsd or the session says autonomous"), which is not measurable.
|
|
85
|
+
`/fsd` now writes a marker at entry (fsd SKILL.md 「무인 모드 마커」) and removes it at convergence.
|
|
86
|
+
|
|
87
|
+
1. Measure (both signals; either one is sufficient):
|
|
88
|
+
marker="/tmp/.claude-fsd-$PPID"; unattended_mode=false; marker_state=absent
|
|
89
|
+
if [ -f "$marker" ]; then
|
|
90
|
+
if [ -n "$(command find "$marker" -mmin +360 2>/dev/null)" ]; then marker_state=stale; else marker_state=present; fi
|
|
91
|
+
fi
|
|
92
|
+
if [ "$marker_state" = "present" ] || [ "${OMCUSTOM_UNATTENDED:-}" = "1" ]; then unattended_mode=true; fi
|
|
93
|
+
echo "unattended_mode=$unattended_mode marker=$marker_state env=${OMCUSTOM_UNATTENDED:-unset}"
|
|
94
|
+
`$PPID` inside the Bash tool is the Claude Code process and is stable across calls, so the
|
|
95
|
+
marker written by `/fsd` in an earlier call is visible here.
|
|
96
|
+
A marker older than 360 minutes is treated as absent (stale guard — crash / PID reuse); `/fsd`
|
|
97
|
+
re-writes the marker at every iteration start. The snippet always exits 0 so a false result is
|
|
98
|
+
not mistaken for a measurement failure.
|
|
99
|
+
|
|
100
|
+
2. Report: "[pre-triage] unattended_mode=<true|false> (marker /tmp/.claude-fsd-$PPID: <present|stale|absent>; OMCUSTOM_UNATTENDED=<value|unset>)"
|
|
101
|
+
|
|
102
|
+
3. Output `unattended_mode` as pipeline state for scope-selection Step 3. Do NOT infer unattended
|
|
103
|
+
mode from prose or from the fact that this pipeline was invoked by a skill — only the two
|
|
104
|
+
measured signals above count.
|
|
105
|
+
|
|
81
106
|
Phase 1 — Ensure required labels exist, then scan issues.
|
|
82
107
|
|
|
83
108
|
1. Create labels if missing (idempotent):
|
|
@@ -167,9 +192,10 @@ steps:
|
|
|
167
192
|
무인 실행(`/fsd` 등 사용자가 실시간 응답할 수 없는 루프)에서는 `.claude/hooks/**` 대상 이슈를
|
|
168
193
|
이번 스코프에서 **분리·이월**하고 나머지 적격 이슈로 진행하며, 반복 경계(homework 게이트)에서
|
|
169
194
|
승인 질문을 **1회** 묶어 제시한다. 승인을 받으면 R015 directive persistence로 세션 내 동일
|
|
170
|
-
카테고리에 지속 적용. 유인 모드에서는 기존대로 즉시 승인 요청. 무인
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
카테고리에 지속 적용. 유인 모드에서는 기존대로 즉시 승인 요청. 무인 판별은 pre-triage Phase 0.6이 실측한
|
|
196
|
+
`unattended_mode` 상태를 사용한다(마커
|
|
197
|
+
`/tmp/.claude-fsd-$PPID` 또는 env `OMCUSTOM_UNATTENDED=1`; #1650 C). 산문 추론("/fsd로 진입한 것
|
|
198
|
+
같다")으로 무인 여부를 판정하지 않는다.
|
|
173
199
|
|
|
174
200
|
Output approval_required_paths (list, empty if none) as pipeline state.
|
|
175
201
|
|
|
@@ -224,7 +250,7 @@ steps:
|
|
|
224
250
|
- triage step: MAY replace professor-triage skill spawn with orchestrator integrated analysis
|
|
225
251
|
- plan step: MAY replace release-plan skill spawn with orchestrator integrated analysis
|
|
226
252
|
- deep-plan step: MAY replace deep-plan skill spawn with orchestrator integrated analysis
|
|
227
|
-
- deep-verify step: perform via mgr-sauron R017 structural verification + core self-check
|
|
253
|
+
- deep-verify step: perform via mgr-sauron R017 structural verification + core self-check (split standard: see the deep-verify step description — R017 spawn + change-type adversarial review, one single-goal delegation each)
|
|
228
254
|
(instead of full deep-verify skill spawn). If the change set has NO structural surface
|
|
229
255
|
(no agent/skill/guide/rule frontmatter changed — e.g. a workflow-yaml-only or docs-only change),
|
|
230
256
|
the mgr-sauron R017 spawn MAY be skipped and replaced by deterministic self-check
|
|
@@ -288,6 +314,13 @@ steps:
|
|
|
288
314
|
1. A scoped issue carries (in its body or a linked artifact) a CONVERGED research/plan output equivalent to the step's skill — e.g. a `/research` multi-team analysis that explicitly converged, a `/deep-plan` artifact, or a prior `professor-triage`/`release-plan` output.
|
|
289
315
|
2. The artifact covers the scoped issues' domain (not stale or unrelated).
|
|
290
316
|
3. The substitution is for a PLANNING/VERIFICATION step ONLY — never for implement, verify-build, release, or ci-check.
|
|
317
|
+
4. When the artifact is a SAME-SESSION triage/plan output substituting for `deep-plan` (or `plan`),
|
|
318
|
+
every implement-stage delegation prompt MUST instruct the agent to re-locate targets by ANCHOR
|
|
319
|
+
(function name / unique string) and treat any line numbers in the issue body or artifact as
|
|
320
|
+
reference only. Line numbers go stale across release commits — v1.1.60: #1647 cited lines
|
|
321
|
+
346-348 from v1.1.59, but the script had grown 608→733 lines and two agents had to re-search
|
|
322
|
+
(#1652 #3-1/#3-2). The justification log MUST name the artifact AND state that anchor-based
|
|
323
|
+
re-measurement was included in the delegation prompts.
|
|
291
324
|
|
|
292
325
|
If substituted, emit a MANDATORY justification log naming the artifact:
|
|
293
326
|
"[compression-mode] step '{step}' satisfied by converged artifact {ref} (substitution); skill spawn skipped"
|
|
@@ -408,7 +441,7 @@ steps:
|
|
|
408
441
|
|
|
409
442
|
- name: deep-verify
|
|
410
443
|
skill: deep-verify
|
|
411
|
-
description: "Multi-angle release quality verification — self-review checklist if docs-only; mgr-sauron R017 + core self-check if lite. MULTI-PHASE: 스킬을 그대로 spawn하지 말고 R020 「위임 경계를 Phase 개수로 설계」에 따라 단일 목표 위임으로 분할해 순차 발주한다 (#1595 #4)."
|
|
444
|
+
description: "Multi-angle release quality verification — self-review checklist if docs-only; mgr-sauron R017 + core self-check if lite. MULTI-PHASE: 스킬을 그대로 spawn하지 말고 R020 「위임 경계를 Phase 개수로 설계」에 따라 단일 목표 위임으로 분할해 순차 발주한다 (#1595 #4). lite 분할 표준(#1652 #3-4): (1) mgr-sauron R017 구조 검증 단일 목표 위임 1건 + (2) 변경 성격별 적대적 리뷰 단일 목표 위임 1건 — 스크립트 변경이면 실행 재현 기반 adversarial-review, 룰/스킬/yaml 텍스트 변경이면 문구 정합·배선 리뷰. 근거: v1.1.59/60 두 반복 연속 적대적 리뷰가 신규 회귀(M-3/M-4, heredoc 위조)를 실행 재현으로 포착."
|
|
412
445
|
depends_on: verify-build
|
|
413
446
|
|
|
414
447
|
- name: release
|
|
@@ -79,6 +79,14 @@ gh issue create \
|
|
|
79
79
|
--body "## 출처\n\nv{version} 릴리즈 워크플로우에서 자동 등록.\n\n## 컨텍스트\n\n{상세 컨텍스트}\n\n## 권장 조치\n\n{권장 사항}" \
|
|
80
80
|
--label "professor"
|
|
81
81
|
```
|
|
82
|
+
|
|
83
|
+
**이슈 본문의 코드 위치 표기 — 행 번호 대신 앵커 (#1652 #3-2)**: `## 컨텍스트`에서 코드 위치는 행
|
|
84
|
+
번호가 아니라 **함수명·고유 앵커 문자열**(예: 함수명, 해당 위치의 고유 주석 문구)로 적는다. 행
|
|
85
|
+
번호는 다음 릴리즈 커밋에서 stale해진다 — v1.1.60 세션에서 #1647 본문의 행 346-348은 v1.1.59 시점
|
|
86
|
+
값이었고, 스크립트가 608→733행으로 성장해 구현 에이전트 2개가 재탐색해야 했다. 행 번호를
|
|
87
|
+
병기하려면 기준 커밋 SHA를 함께 적고 "참고용"임을 명시한다. 후속 위임서도 "행 번호는 참고, 앵커로
|
|
88
|
+
재탐색"을 전제로 작성된다(auto-dev.yaml substitution 조항 cross-ref).
|
|
89
|
+
|
|
82
90
|
Add priority label (`P3` default for defects surfaced here; escalate to `P2` if MEDIUM+ severity).
|
|
83
91
|
|
|
84
92
|
**Authority**: user directive (session 102) — genuine defects found during release workflows should be registered without prompting. See also R016 Defect Response Matrix: CI/infra defect and Process gap both require Issue registration.
|
package/templates/manifest.json
CHANGED
|
@@ -78,6 +78,31 @@ steps:
|
|
|
78
78
|
4. Do NOT read, echo, or modify any credential material while checking settings — read the
|
|
79
79
|
single `permissions.defaultMode` field only (R001).
|
|
80
80
|
|
|
81
|
+
Phase 0.6 — Detect unattended mode deterministically (#1650 C).
|
|
82
|
+
|
|
83
|
+
Why: scope-selection Step 3 defers `.claude/hooks/**` issues in unattended runs. Until v1.1.60 the
|
|
84
|
+
signal was prose ("entered via /fsd or the session says autonomous"), which is not measurable.
|
|
85
|
+
`/fsd` now writes a marker at entry (fsd SKILL.md 「무인 모드 마커」) and removes it at convergence.
|
|
86
|
+
|
|
87
|
+
1. Measure (both signals; either one is sufficient):
|
|
88
|
+
marker="/tmp/.claude-fsd-$PPID"; unattended_mode=false; marker_state=absent
|
|
89
|
+
if [ -f "$marker" ]; then
|
|
90
|
+
if [ -n "$(command find "$marker" -mmin +360 2>/dev/null)" ]; then marker_state=stale; else marker_state=present; fi
|
|
91
|
+
fi
|
|
92
|
+
if [ "$marker_state" = "present" ] || [ "${OMCUSTOM_UNATTENDED:-}" = "1" ]; then unattended_mode=true; fi
|
|
93
|
+
echo "unattended_mode=$unattended_mode marker=$marker_state env=${OMCUSTOM_UNATTENDED:-unset}"
|
|
94
|
+
`$PPID` inside the Bash tool is the Claude Code process and is stable across calls, so the
|
|
95
|
+
marker written by `/fsd` in an earlier call is visible here.
|
|
96
|
+
A marker older than 360 minutes is treated as absent (stale guard — crash / PID reuse); `/fsd`
|
|
97
|
+
re-writes the marker at every iteration start. The snippet always exits 0 so a false result is
|
|
98
|
+
not mistaken for a measurement failure.
|
|
99
|
+
|
|
100
|
+
2. Report: "[pre-triage] unattended_mode=<true|false> (marker /tmp/.claude-fsd-$PPID: <present|stale|absent>; OMCUSTOM_UNATTENDED=<value|unset>)"
|
|
101
|
+
|
|
102
|
+
3. Output `unattended_mode` as pipeline state for scope-selection Step 3. Do NOT infer unattended
|
|
103
|
+
mode from prose or from the fact that this pipeline was invoked by a skill — only the two
|
|
104
|
+
measured signals above count.
|
|
105
|
+
|
|
81
106
|
Phase 1 — Ensure required labels exist, then scan issues.
|
|
82
107
|
|
|
83
108
|
1. Create labels if missing (idempotent):
|
|
@@ -167,9 +192,10 @@ steps:
|
|
|
167
192
|
무인 실행(`/fsd` 등 사용자가 실시간 응답할 수 없는 루프)에서는 `.claude/hooks/**` 대상 이슈를
|
|
168
193
|
이번 스코프에서 **분리·이월**하고 나머지 적격 이슈로 진행하며, 반복 경계(homework 게이트)에서
|
|
169
194
|
승인 질문을 **1회** 묶어 제시한다. 승인을 받으면 R015 directive persistence로 세션 내 동일
|
|
170
|
-
카테고리에 지속 적용. 유인 모드에서는 기존대로 즉시 승인 요청. 무인
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
카테고리에 지속 적용. 유인 모드에서는 기존대로 즉시 승인 요청. 무인 판별은 pre-triage Phase 0.6이 실측한
|
|
196
|
+
`unattended_mode` 상태를 사용한다(마커
|
|
197
|
+
`/tmp/.claude-fsd-$PPID` 또는 env `OMCUSTOM_UNATTENDED=1`; #1650 C). 산문 추론("/fsd로 진입한 것
|
|
198
|
+
같다")으로 무인 여부를 판정하지 않는다.
|
|
173
199
|
|
|
174
200
|
Output approval_required_paths (list, empty if none) as pipeline state.
|
|
175
201
|
|
|
@@ -224,7 +250,7 @@ steps:
|
|
|
224
250
|
- triage step: MAY replace professor-triage skill spawn with orchestrator integrated analysis
|
|
225
251
|
- plan step: MAY replace release-plan skill spawn with orchestrator integrated analysis
|
|
226
252
|
- deep-plan step: MAY replace deep-plan skill spawn with orchestrator integrated analysis
|
|
227
|
-
- deep-verify step: perform via mgr-sauron R017 structural verification + core self-check
|
|
253
|
+
- deep-verify step: perform via mgr-sauron R017 structural verification + core self-check (split standard: see the deep-verify step description — R017 spawn + change-type adversarial review, one single-goal delegation each)
|
|
228
254
|
(instead of full deep-verify skill spawn). If the change set has NO structural surface
|
|
229
255
|
(no agent/skill/guide/rule frontmatter changed — e.g. a workflow-yaml-only or docs-only change),
|
|
230
256
|
the mgr-sauron R017 spawn MAY be skipped and replaced by deterministic self-check
|
|
@@ -288,6 +314,13 @@ steps:
|
|
|
288
314
|
1. A scoped issue carries (in its body or a linked artifact) a CONVERGED research/plan output equivalent to the step's skill — e.g. a `/research` multi-team analysis that explicitly converged, a `/deep-plan` artifact, or a prior `professor-triage`/`release-plan` output.
|
|
289
315
|
2. The artifact covers the scoped issues' domain (not stale or unrelated).
|
|
290
316
|
3. The substitution is for a PLANNING/VERIFICATION step ONLY — never for implement, verify-build, release, or ci-check.
|
|
317
|
+
4. When the artifact is a SAME-SESSION triage/plan output substituting for `deep-plan` (or `plan`),
|
|
318
|
+
every implement-stage delegation prompt MUST instruct the agent to re-locate targets by ANCHOR
|
|
319
|
+
(function name / unique string) and treat any line numbers in the issue body or artifact as
|
|
320
|
+
reference only. Line numbers go stale across release commits — v1.1.60: #1647 cited lines
|
|
321
|
+
346-348 from v1.1.59, but the script had grown 608→733 lines and two agents had to re-search
|
|
322
|
+
(#1652 #3-1/#3-2). The justification log MUST name the artifact AND state that anchor-based
|
|
323
|
+
re-measurement was included in the delegation prompts.
|
|
291
324
|
|
|
292
325
|
If substituted, emit a MANDATORY justification log naming the artifact:
|
|
293
326
|
"[compression-mode] step '{step}' satisfied by converged artifact {ref} (substitution); skill spawn skipped"
|
|
@@ -408,7 +441,7 @@ steps:
|
|
|
408
441
|
|
|
409
442
|
- name: deep-verify
|
|
410
443
|
skill: deep-verify
|
|
411
|
-
description: "Multi-angle release quality verification — self-review checklist if docs-only; mgr-sauron R017 + core self-check if lite. MULTI-PHASE: 스킬을 그대로 spawn하지 말고 R020 「위임 경계를 Phase 개수로 설계」에 따라 단일 목표 위임으로 분할해 순차 발주한다 (#1595 #4)."
|
|
444
|
+
description: "Multi-angle release quality verification — self-review checklist if docs-only; mgr-sauron R017 + core self-check if lite. MULTI-PHASE: 스킬을 그대로 spawn하지 말고 R020 「위임 경계를 Phase 개수로 설계」에 따라 단일 목표 위임으로 분할해 순차 발주한다 (#1595 #4). lite 분할 표준(#1652 #3-4): (1) mgr-sauron R017 구조 검증 단일 목표 위임 1건 + (2) 변경 성격별 적대적 리뷰 단일 목표 위임 1건 — 스크립트 변경이면 실행 재현 기반 adversarial-review, 룰/스킬/yaml 텍스트 변경이면 문구 정합·배선 리뷰. 근거: v1.1.59/60 두 반복 연속 적대적 리뷰가 신규 회귀(M-3/M-4, heredoc 위조)를 실행 재현으로 포착."
|
|
412
445
|
depends_on: verify-build
|
|
413
446
|
|
|
414
447
|
- name: release
|