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.
Files changed (26) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/index.js +1 -1
  3. package/package.json +1 -1
  4. package/templates/.claude/hooks/scripts/agent-start-recorder.sh +4 -0
  5. package/templates/.claude/hooks/scripts/agent-teams-advisor.sh +6 -2
  6. package/templates/.claude/hooks/scripts/audit-log.sh +19 -2
  7. package/templates/.claude/hooks/scripts/content-hash-validator.sh +6 -2
  8. package/templates/.claude/hooks/scripts/model-escalation-advisor.sh +6 -2
  9. package/templates/.claude/hooks/scripts/playwright-compress.sh +25 -2
  10. package/templates/.claude/hooks/scripts/r007-r008-drift-advisor.sh +72 -2
  11. package/templates/.claude/hooks/scripts/schema-validator.sh +14 -9
  12. package/templates/.claude/hooks/scripts/secret-filter.sh +46 -4
  13. package/templates/.claude/hooks/scripts/session-reflection.sh +5 -1
  14. package/templates/.claude/hooks/scripts/stuck-detector.sh +43 -11
  15. package/templates/.claude/hooks/scripts/task-outcome-recorder.sh +16 -9
  16. package/templates/.claude/rules/MUST-completion-verification.md +4 -0
  17. package/templates/.claude/rules/MUST-enforcement-policy.md +1 -1
  18. package/templates/.claude/rules/MUST-orchestrator-coordination.md +2 -0
  19. package/templates/.claude/rules/MUST-sync-verification.md +4 -0
  20. package/templates/.claude/rules/MUST-tool-identification.md +11 -0
  21. package/templates/.claude/rules/SHOULD-memory-integration.md +3 -0
  22. package/templates/.claude/skills/fsd/SKILL.md +40 -2
  23. package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +38 -5
  24. package/templates/.claude/skills/post-release-followup/SKILL.md +8 -0
  25. package/templates/manifest.json +1 -1
  26. package/templates/workflows/auto-dev.yaml +38 -5
package/dist/cli/index.js CHANGED
@@ -243,7 +243,7 @@ var init_package = __esm(() => {
243
243
  workspaces: [
244
244
  "packages/*"
245
245
  ],
246
- version: "1.1.60",
246
+ version: "1.1.62",
247
247
  description: "Batteries-included agent harness for Claude Code",
248
248
  type: "module",
249
249
  bin: {
package/dist/index.js CHANGED
@@ -2539,7 +2539,7 @@ var package_default = {
2539
2539
  workspaces: [
2540
2540
  "packages/*"
2541
2541
  ],
2542
- version: "1.1.60",
2542
+ version: "1.1.62",
2543
2543
  description: "Batteries-included agent harness for Claude Code",
2544
2544
  type: "module",
2545
2545
  bin: {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.60",
6
+ "version": "1.1.62",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -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
- is_error=$(printf '%s\n' "$input" | jq -r '.tool_output.is_error // false')
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
- tool_output=$(printf '%s\n' "$input" | jq -r '.tool_output // ""')
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
- meta=$(printf '%s' "$input" | jq -r '[(.session_id // ""), (.hook_event_name // ""), (.transcript_path // "")] | @tsv' 2>/dev/null) || exit 0
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
- | ($an_tool + (if $an_spawn_item > 0 then $an_spawn_item else $an_spawn_hdr end)) as $announce
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
- # Extract tool info
16
- tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name // "unknown"')
17
- tool_input=$(printf '%s\n' "$input" | jq -r '.tool_input // {}')
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
- # Extract output to scan
16
- tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name // "unknown"')
17
- output=$(printf '%s\n' "$input" | jq -r '.tool_output.output // ""')
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
- tok="$(printf '%s' "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
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
- if [ "$line" != "${line%<<*}" ]; then
230
- before_op="${line%<<*}"
231
- dq="$(printf '%s' "$before_op" | tr -cd '"' | wc -c | tr -d '[:space:]')"
232
- sq="$(printf '%s' "$before_op" | tr -cd "'" | wc -c | tr -d '[:space:]')"
233
- if [ $((dq % 2)) -eq 1 ] || [ $((sq % 2)) -eq 1 ]; then
234
- printf '%s' "${out}__QUOTED_HEREDOC_OPENER__"$'\n'
235
- return 0
236
- fi
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
- seg="$(printf '%s' "$seg" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
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
- # Extract task info — support both PostToolUse (tool_input.*) and SubagentStop (top-level) shapes
15
- agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // .agent_type // "unknown"')
16
- model=$(printf '%s\n' "$input" | jq -r '.tool_input.model // .model // "inherit"')
17
- description=$(printf '%s\n' "$input" | jq -r '.tool_input.description // .description // ""' | head -c 80)
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 → next iteration
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
- `/fsd`로 진입했거나 세션 지침이 autonomous임을 명시한 경우"로 판단한다 — 결정론적 판별 신호
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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.60",
2
+ "version": "1.1.62",
3
3
  "lastUpdated": "2026-09-03",
4
4
  "omcustomMinClaudeCode": "2.1.121",
5
5
  "omcustomMinClaudeCodeReason": "Sensitive-path direct Write/Edit on .claude/** under bypassPermissions (R010 deprecation, #1101)",
@@ -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
- `/fsd`로 진입했거나 세션 지침이 autonomous임을 명시한 경우"로 판단한다 — 결정론적 판별 신호
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