@ccoalm/ccl-skills 0.18.5 → 0.18.7

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 (56) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/agent-context/session-policy.md +5 -4
  2. package/dist/assets/marketplace/plugins/ccl-skills/agent-context/session-start.md +11 -9
  3. package/dist/assets/marketplace/plugins/ccl-skills/hooks/host-input.py +135 -11
  4. package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-review-covers-head.sh +34 -9
  5. package/dist/assets/marketplace/plugins/ccl-skills/hooks/skill-extraction-gate-stop.sh +14 -3
  6. package/dist/assets/marketplace/plugins/ccl-skills/hooks/skill-loading.py +39 -2
  7. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_host_input.py +11 -0
  8. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_proposed_next.py +180 -3
  9. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_review_covers_head.sh +19 -3
  10. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_skill_loading.py +77 -0
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/development-completion.md +4 -3
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/AGENTS.md +11 -1
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_review.sh +65 -11
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +4 -1
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_cli_review_wrappers.sh +158 -6
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +39 -1
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +48 -2
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +5 -5
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-review-gate-mechanics.md +1 -1
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/pre-final-continuation-gate.md +4 -3
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/review-reception.md +1 -1
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/SKILL.md +1 -1
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +1 -1
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +4 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +1 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +36 -0
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +2 -2
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +2 -0
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-size-budget.sh +2 -0
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-sync-pointers.sh +2 -0
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing-bank.rb +11 -2
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_body_compliance_grading.sh +79 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +4 -2
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_register_pending_exclusion.sh +2 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +4 -0
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_route_drift.sh +2 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_size_budget.sh +4 -2
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_source_register_lifecycle.sh +9 -1
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_sync_pointers.sh +2 -0
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_grader_diagnostics.sh +2 -0
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_resolution.sh +32 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_surface_binding.sh +2 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_prose_target.sh +2 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_dateless_host.sh +3 -1
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_verdict_differential.sh +2 -0
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_round_attribution.sh +2 -0
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +2 -0
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_source_refuted.sh +2 -0
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_locale_independent_gates.sh +264 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_resolution.sh +2 -0
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +2 -0
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_delivery_contract.sh +2 -0
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_loading_budget.sh +2 -0
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate-skill.sh +2 -0
  55. package/dist/assets/release.json +61 -56
  56. package/package.json +1 -1
@@ -141,13 +141,190 @@ class ProposedNextTests(unittest.TestCase):
141
141
  self.payload['last_assistant_message'] = text
142
142
  self.assert_block(self.run_hook())
143
143
  for text in ('proposed-next: none — status only', '**proposed-next:** none — status only',
144
- 'proposed-next: blocked: need an explicit decision',
145
- 'proposed-next: none — awaiting approval',
146
- 'proposed-next: none - waiting for a resource'):
144
+ 'proposed-next: none — all requested work is done and verified'):
147
145
  with self.subTest(text=text):
148
146
  self.payload['last_assistant_message'] = text
149
147
  self.assertEqual(self.run_hook(), {})
150
148
 
149
+ def assert_decision_recheck(self, payload):
150
+ result = self.run_hook(payload)
151
+ self.assert_block(result)
152
+ self.assertIn('Decision recheck', result['reason'])
153
+ self.assertIn('security', result['reason'])
154
+ self.assertIn('supplies no new goal or authorization', result['reason'])
155
+ self.assertEqual(self.run_hook(dict(payload, stop_hook_active=True)), {})
156
+
157
+ def test_user_dependent_stop_rechecks_the_blocker_once(self):
158
+ for events in ([], self.claude_load()):
159
+ self.events(events)
160
+ for text in ('proposed-next: blocked: need an explicit decision',
161
+ 'proposed-next: none — awaiting approval',
162
+ 'proposed-next: none - waiting for a resource',
163
+ 'proposed-next: none — 等待确认安全负责人'):
164
+ with self.subTest(text=text):
165
+ self.assert_decision_recheck(dict(self.payload, last_assistant_message=text))
166
+
167
+ def test_permission_question_after_edits_rechecks_instead_of_formatting(self):
168
+ for events in (self.claude_load(), self.edit_events()):
169
+ self.events(events)
170
+ for text in ('Patch is ready. Should I proceed with the remaining tests?',
171
+ 'Done with step 1. Do you want me to continue?',
172
+ '第一步已完成,是否继续?', '安全风险需要你决定,要不要我修改?',
173
+ 'Patch ready. **Should I proceed?**', '**是否继续?**',
174
+ 'Patch ready. __Should I proceed?__', '_是否继续?_',
175
+ '***Should I proceed?***', '*是否继续?*',
176
+ '**是否继续?**\nproposed-next: none — status only'):
177
+ with self.subTest(text=text):
178
+ self.assert_decision_recheck(dict(self.payload, last_assistant_message=text))
179
+ self.events([])
180
+ for text in ('Should I proceed?', '是否继续?', '**Should I proceed?**', '_是否继续?_'):
181
+ self.payload['last_assistant_message'] = text
182
+ self.assertEqual(self.run_hook(), {})
183
+ self.events(self.edit_events())
184
+ self.payload['last_assistant_message'] = 'Done; checks passed.'
185
+ self.assertEqual(self.run_hook(), {})
186
+
187
+ def test_long_permission_lines_finish_within_hook_timeout(self):
188
+ self.events(self.edit_events())
189
+ prefix = 'Should I do this; ' * 60000
190
+ for suffix, expected in (('', None), ('continue?', 'block')):
191
+ with self.subTest(terminal_question=bool(suffix)):
192
+ payload = dict(self.payload, last_assistant_message=prefix + suffix)
193
+ result = subprocess.run(
194
+ ['python3', str(self.hooks / 'host-input.py'), 'proposed-next'],
195
+ input=json.dumps(payload), text=True, capture_output=True,
196
+ cwd=self.root, timeout=5)
197
+ self.assertEqual(result.returncode, 0, result.stderr)
198
+ self.assertEqual(result.stderr, '')
199
+ value = json.loads(result.stdout) if result.stdout else {}
200
+ self.assertEqual(value.get('decision'), expected)
201
+
202
+ def test_finished_none_states_are_not_waits(self):
203
+ self.events(self.claude_load())
204
+ for text in ('proposed-next: none — PR approved and merged',
205
+ 'proposed-next: none — tests confirm the fix',
206
+ 'proposed-next: none — permission tests added',
207
+ 'proposed-next: none — authorization module refactored',
208
+ 'proposed-next: none — status only; awaiting nothing',
209
+ 'proposed-next: none — 已确认完成',
210
+ 'proposed-next: none — 已批准并合并',
211
+ 'proposed-next: none — merged after your approval',
212
+ 'proposed-next: none — all done; let me know if you need more',
213
+ 'proposed-next: none — fixed the await bug in fetch()',
214
+ 'proposed-next: none — 修复了等待超时',
215
+ 'proposed-next: none — 审批流程已实现',
216
+ 'proposed-next: none — PR opened; awaiting review',
217
+ "Done.\nLet me know if you'd like me to make any other changes.\nproposed-next: none — complete",
218
+ 'proposed-next: none — 完成,期待你的反馈', 'proposed-next: none — 已按你定义的接口实现',
219
+ 'proposed-next: none — merged based on your approval'):
220
+ with self.subTest(text=text):
221
+ self.assertEqual(self.run_hook(dict(self.payload, last_assistant_message=text)), {})
222
+
223
+ def test_more_user_waits_and_labelled_questions_are_rechecked(self):
224
+ self.events(self.claude_load())
225
+ for text in ('proposed-next: none — your call', 'proposed-next: none — needs sign-off',
226
+ 'proposed-next: none — 等你拍板', 'proposed-next: none — 需要你审批',
227
+ 'Patch ready. Should I push it?\nproposed-next: none — work is ready',
228
+ 'Ready to merge. OK to merge?', 'Should I proceed? Or stop?',
229
+ 'Should I push the branch?\nproposed-next: none — status only',
230
+ 'proposed-next: none — 待确认', 'proposed-next: none — pending approval',
231
+ 'proposed-next: none — decision pending', 'proposed-next: none — 负责人未定',
232
+ 'proposed-next: none — need the owner to sign off', 'proposed-next: none — 请选择方案 A 或 B',
233
+ '已改完。继续?\nproposed-next: none — 改动已完成', '要不要我继续\nproposed-next: none — 改动已完成',
234
+ 'proposed-next: none — 由你决定', 'proposed-next: none — up to you',
235
+ 'proposed-next: none — need access to prod', 'Done — should I push?\nproposed-next: none — ready',
236
+ '我可以继续吗?\nproposed-next: none — 改动已完成'):
237
+ with self.subTest(text=text):
238
+ self.assert_decision_recheck(dict(self.payload, last_assistant_message=text))
239
+
240
+ def test_announced_next_steps_after_work_recheck_instead_of_stopping(self):
241
+ plan = ('已完成两项检查。下一步:\n1. 补回归用例\n2. 重跑本地套件\n3. 跑一次付费对照生成\n'
242
+ '先推进前两步;付费对照还需要确定费用上限。')
243
+ for events in (self.edit_events(), self.claude_load(), self.codex_read()):
244
+ self.events(events)
245
+ for text in (plan, "Patch applied. Next, I'll rerun the suite and update the docs.",
246
+ '接下来我会补测试并重跑。', 'Now let me run the remaining checks.',
247
+ 'The config change is in. I will now update the migration.',
248
+ 'Step 1 is done; the paid comparison run still needs to settle a budget cap.',
249
+ '**接下来我先修复失败用例。**', '第一步完成,我马上跑回归。',
250
+ '下一步我先跑回归。\nproposed-next: none — status only'):
251
+ with self.subTest(text=text):
252
+ payload = dict(self.payload, last_assistant_message=text)
253
+ self.assert_decision_recheck(payload)
254
+ self.assertIn('not a stopping point', self.run_hook(payload)['reason'])
255
+
256
+ def test_optional_clause_does_not_hide_an_unconditional_next_step(self):
257
+ self.events(self.edit_events())
258
+ for text in ("Patch applied. Next, I'll rerun the tests; let me know if you need anything else.",
259
+ "Next, I'll rerun the tests. If you want, I can also add a flag.",
260
+ "Let me know if you need a flag; next, I'll rerun the tests.",
261
+ '改动已完成,接下来我会跑回归;如需其他调整请告诉我。',
262
+ '如需其他调整请告诉我。接下来我会跑回归。'):
263
+ with self.subTest(text=text):
264
+ self.assert_decision_recheck(dict(self.payload, last_assistant_message=text))
265
+
266
+ def test_condition_applies_to_its_own_announced_step(self):
267
+ self.events(self.edit_events())
268
+ for text in ("If you want, next I'll add an optional flag.",
269
+ "Next, I'll add the flag if you want it.",
270
+ '如果你需要,接下来我会补文档。',
271
+ '接下来我会补文档,如果你需要的话。'):
272
+ with self.subTest(text=text):
273
+ self.assertEqual(self.run_hook(dict(self.payload, last_assistant_message=text)), {})
274
+
275
+ def test_routine_test_permission_recheck_does_not_invent_a_cost_blocker(self):
276
+ self.events(self.edit_events())
277
+ for text in ('proposed-next: blocked: 小额测试等待你批准费用上限',
278
+ '测试环境的回归已准备好,是否开始?',
279
+ '开发环境验证准备完成,请确认授权?',
280
+ 'Should I start the small model smoke test?'):
281
+ with self.subTest(text=text):
282
+ payload = dict(self.payload, last_assistant_message=text)
283
+ self.assert_decision_recheck(payload)
284
+ reason = self.run_hook(payload)['reason']
285
+ self.assertIn('Small tests and routine development/test-environment operations', reason)
286
+ self.assertIn('explicit user cost or run-count limits', reason)
287
+ self.assertNotIn('such as a cost cap or a paid run', reason)
288
+
289
+ def test_finished_reports_offers_and_quoted_plans_are_not_announcements(self):
290
+ self.events(self.edit_events())
291
+ for text in ('Done; all checks passed.', "If you want, I'll also add a CLI flag.",
292
+ '如需,我可以接着补文档。', '修复完成。我已经补了测试并重跑。',
293
+ 'Earlier I said I would rerun the suite; it now passes.',
294
+ "Let me know if you need anything else and I'll start on it.",
295
+ '> 接下来我会补测试', "```text\nNext, I'll run the suite\n```",
296
+ "Plan was: next, I'll fix A.\nFixed A.\nFixed B.\nAll checks pass."):
297
+ with self.subTest(text=text):
298
+ self.assertEqual(self.run_hook(dict(self.payload, last_assistant_message=text)), {})
299
+ self.events([])
300
+ for text in ('接下来我会解释这个错误的含义。', "Next, I'll explain the error."):
301
+ with self.subTest(text=text, transcript='no work evidence'):
302
+ self.assertEqual(self.run_hook(dict(self.payload, last_assistant_message=text)), {})
303
+ self.assertEqual(self.run_hook(dict(
304
+ self.payload, last_assistant_message=text + '\nproposed-next: none — status only')), {})
305
+
306
+ def test_pleasantries_and_quoted_questions_after_edits_are_not_permission_asks(self):
307
+ self.events(self.edit_events())
308
+ for text in ('Done. Can I help with anything else?',
309
+ 'Fixed. Q: why does continue fail?',
310
+ 'Done.\n> Should I proceed?',
311
+ '这个问题是否已修复?', 'How should I interpret this error?',
312
+ 'What is the default if I go ahead without a flag?',
313
+ 'Does this look ok to you?', '不管要不要我做都行。',
314
+ '> **Should I proceed?**', '```text\n**是否继续?**\n```',
315
+ '`**Should I proceed?**`', '**Done; checks passed.**',
316
+ '__Done; checks passed.__'):
317
+ with self.subTest(text=text):
318
+ self.assertEqual(self.run_hook(dict(self.payload, last_assistant_message=text)), {})
319
+
320
+ def edit_events(self):
321
+ target = str(self.root / 'src.txt')
322
+ return [
323
+ {'type': 'assistant', 'message': {'content': [{'type': 'tool_use', 'id': 'edit',
324
+ 'name': 'Edit', 'input': {'file_path': target, 'old_string': 'a', 'new_string': 'b'}}]}},
325
+ {'type': 'user', 'message': {'content': [{'type': 'tool_result',
326
+ 'tool_use_id': 'edit', 'is_error': False, 'content': 'updated'}]}}]
327
+
151
328
  def test_actionable_handoff_rechecks_continuation_instead_of_silently_stopping(self):
152
329
  for events in ([], self.claude_load(), self.codex_read()):
153
330
  self.events(events)
@@ -24,10 +24,11 @@ git -C "$repo" config user.name 'Test User'
24
24
  commit() { printf '%s\n' "$2" >"$repo/$1"; git -C "$repo" add -A; git -C "$repo" commit -q -m "$3"; }
25
25
  commit code.txt one "first change"
26
26
 
27
- write_receipt() { # <head> <worktree_clean true|false>
27
+ write_receipt() { # <head> <worktree_clean true|false> [status | none to omit; default passed]
28
28
  mkdir -p "$repo/.git/ccl-code-review"
29
- jq -nc --arg h "$1" --argjson c "$2" \
30
- '{schema_version:1,head:$h,worktree_clean:$c,mode:"review",status:"findings",recorded_at:"2026-01-01T00:00:00Z"}' \
29
+ jq -nc --arg h "$1" --argjson c "$2" --arg s "${3:-passed}" \
30
+ '{schema_version:1,head:$h,worktree_clean:$c,mode:"review",status:$s,recorded_at:"2026-01-01T00:00:00Z"}
31
+ | if .status == "none" then del(.status) else . end' \
31
32
  >"$repo/.git/ccl-code-review/last-review.json"
32
33
  }
33
34
 
@@ -45,6 +46,7 @@ probe() {
45
46
  }
46
47
 
47
48
  probe "no receipt" remind 'glab mr create --title x' "$repo" '没有记录到任何结论性的 code-review 结果'
49
+ probe "no receipt names the path it checked" remind 'glab mr create --title x' "$repo" 'ccl-code-review/last-review.json'
48
50
  head1=$(git -C "$repo" rev-parse HEAD)
49
51
  write_receipt "$head1" true
50
52
  probe "covered head" quiet 'glab mr create --title x'
@@ -53,6 +55,18 @@ probe "commit in the same command" remind 'git add -A && git commit -m fix && gh
53
55
  probe "HEAD moves only after the PR opens" quiet 'gh pr create --fill && git checkout main'
54
56
  probe "draft, commit, then ready" remind 'gh pr create --draft --fill && git add -A && git commit -m fix && git push && gh pr ready' "$repo" '会先改动 HEAD'
55
57
 
58
+ # Coverage is not disposition: findings on the covered HEAD still remind.
59
+ write_receipt "$head1" true findings
60
+ probe "findings on the covered head" remind 'glab mr merge 8 --yes' "$repo" '结论是 findings,不是 passed'
61
+ probe "findings reminder names the receipt it read" remind 'gh pr ready 12' "$repo" 'ccl-code-review/last-review.json'
62
+ write_receipt "$head1" true none
63
+ [ "$(jq -r 'has("status")' "$repo/.git/ccl-code-review/last-review.json")" = false ] \
64
+ || { echo "FAIL: fixture still carries a status" >&2; exit 1; }
65
+ probe "a receipt without a status is not a pass" remind 'glab mr create --title x' "$repo" '不是 passed'
66
+ probe "a missing status is not reported as findings" remind 'glab mr create --title x' "$repo" '没有记录可识别的结论'
67
+ write_receipt "$head1" true
68
+ probe "passed on the covered head" quiet 'glab mr merge 8 --yes'
69
+
56
70
  commit test.txt added "add regression test after review"
57
71
  probe "commit after review" remind 'glab mr create --title x' "$repo" 'add regression test after review'
58
72
  probe "gh pr ready" remind 'gh pr ready 12'
@@ -82,6 +96,8 @@ head2=$(git -C "$repo" rev-parse HEAD)
82
96
  write_receipt "$head2" true
83
97
  git -C "$repo" commit -q --amend -m "reworded message only"
84
98
  probe "message-only amend keeps the reviewed tree" quiet 'glab mr create --title x'
99
+ write_receipt "$head2" true findings
100
+ probe "message-only amend keeps the findings too" remind 'glab mr create --title x' "$repo" '结论是 findings,不是 passed'
85
101
 
86
102
  # Rewritten history with different content: the reviewed commit is gone.
87
103
  git -C "$repo" reset -q --hard "$head1"
@@ -77,6 +77,83 @@ class SkillLoadingTests(unittest.TestCase):
77
77
  self.assertNotIn('permissionDecision":"ask', json.dumps(result))
78
78
  self.assertEqual(self.run_hook(self.payload()), {})
79
79
 
80
+ def ccl_checkout(self):
81
+ root = self.root / 'ccl'
82
+ (root / 'skills' / 'skill-extraction-workflow').mkdir(parents=True)
83
+ (root / 'skills' / 'skill-extraction-workflow' / 'SKILL.md').write_text('marker')
84
+ (root / 'skills' / 'sample-owner' / 'references').mkdir(parents=True)
85
+ (root / 'hooks').mkdir()
86
+ return root
87
+
88
+ def extraction_loaded(self, ident='ext'):
89
+ self.append({'type': 'assistant', 'message': {'content': [
90
+ {'type': 'tool_use', 'id': ident, 'name': 'Skill',
91
+ 'input': {'skill': 'ccl-skills:skill-extraction-workflow'}}]}},
92
+ {'type': 'user', 'message': {'content': [
93
+ {'type': 'tool_result', 'tool_use_id': ident, 'content': 'Loaded owner'}]}})
94
+
95
+ def test_shared_skill_edit_names_the_extraction_owner_at_the_first_edit(self):
96
+ root = self.ccl_checkout()
97
+ target = root / 'skills' / 'sample-owner' / 'scripts' / 'gate.sh'
98
+ target.parent.mkdir(parents=True)
99
+ result = self.run_hook(self.payload(tool_input={'file_path': str(target)}))
100
+ self.assertEqual(self.decision(result), 'deny')
101
+ reason = result['hookSpecificOutput']['permissionDecisionReason']
102
+ self.assertIn('skill-extraction-workflow', reason)
103
+ self.assertIn('charter', reason.lower())
104
+
105
+ def test_shared_skill_markdown_edit_reaches_the_checkpoint(self):
106
+ root = self.ccl_checkout()
107
+ target = root / 'skills' / 'sample-owner' / 'references' / 'rule.md'
108
+ result = self.run_hook(self.payload(tool_input={'file_path': str(target)}))
109
+ self.assertEqual(self.decision(result), 'deny')
110
+ self.assertIn('skill-extraction-workflow',
111
+ result['hookSpecificOutput']['permissionDecisionReason'])
112
+
113
+ def test_markdown_outside_a_ccl_checkout_still_skips_the_checkpoint(self):
114
+ outside = self.root / 'product' / 'skills' / 'thing' / 'notes.md'
115
+ outside.parent.mkdir(parents=True)
116
+ self.assertEqual(self.run_hook(self.payload(tool_input={'file_path': str(outside)})), {})
117
+ cached = self.ccl_checkout() / 'skills' / 'sample-owner' / 'SKILL.md'
118
+ # An installed plugin copy carries the same marker; only the cache exclusion skips it.
119
+ cache_root = self.root / 'plugins' / 'cache' / 'ccl'
120
+ (cache_root / 'skills' / 'skill-extraction-workflow').mkdir(parents=True)
121
+ (cache_root / 'skills' / 'skill-extraction-workflow' / 'SKILL.md').write_text('marker')
122
+ plugin_cache = cache_root / 'skills' / 'x' / 'SKILL.md'
123
+ plugin_cache.parent.mkdir(parents=True)
124
+ self.assertEqual(self.run_hook(self.payload(tool_input={'file_path': str(plugin_cache)})), {})
125
+ # The checkpoint is still unspent for a real shared-skill edit.
126
+ result = self.run_hook(self.payload(tool_input={'file_path': str(cached)}))
127
+ self.assertEqual(self.decision(result), 'deny')
128
+
129
+ def test_checkout_under_an_ancestor_named_like_a_surface_is_recognised(self):
130
+ root = self.root / 'skills' / 'ccl'
131
+ (root / 'skills' / 'skill-extraction-workflow').mkdir(parents=True)
132
+ (root / 'skills' / 'skill-extraction-workflow' / 'SKILL.md').write_text('marker')
133
+ target = root / 'skills' / 'sample-owner' / 'SKILL.md'
134
+ target.parent.mkdir(parents=True)
135
+ result = self.run_hook(self.payload(tool_input={'file_path': str(target)}))
136
+ self.assertEqual(self.decision(result), 'deny')
137
+ self.assertIn('skill-extraction-workflow',
138
+ result['hookSpecificOutput']['permissionDecisionReason'])
139
+
140
+ def test_codex_install_copy_is_exempt_like_the_plugin_cache(self):
141
+ install = self.root / '.codex' / 'ccl'
142
+ (install / 'skills' / 'skill-extraction-workflow').mkdir(parents=True)
143
+ (install / 'skills' / 'skill-extraction-workflow' / 'SKILL.md').write_text('marker')
144
+ target = install / 'skills' / 'x' / 'SKILL.md'
145
+ target.parent.mkdir(parents=True)
146
+ self.assertEqual(self.run_hook(self.payload(tool_input={'file_path': str(target)})), {})
147
+ self.assertEqual(self.decision(self.run_hook(self.payload())), 'deny')
148
+
149
+ def test_shared_skill_edit_with_the_owner_loaded_keeps_the_generic_reason(self):
150
+ root = self.ccl_checkout()
151
+ self.extraction_loaded()
152
+ target = root / 'hooks' / 'gate.sh'
153
+ result = self.run_hook(self.payload(tool_input={'file_path': str(target)}))
154
+ self.assertEqual(self.decision(result), 'deny')
155
+ self.assertNotIn('charter', result['hookSpecificOutput']['permissionDecisionReason'].lower())
156
+
80
157
  def test_native_patch_and_all_targets(self):
81
158
  patch = '*** Begin Patch\n*** Add File: README.md\n+doc\n*** Add File: src/new.ts\n+code\n*** End Patch'
82
159
  value = self.run_hook(self.payload('apply_patch', tool_input={'command': patch}))
@@ -5,9 +5,10 @@ This transition applies across implementation owners, including narrow fixes and
5
5
  ## Before invoking review
6
6
 
7
7
  1. Recover the current task, actual diff, scope and authorization. Changing the implementation or scope reopens this check; an earlier plan review cannot discharge review of the resulting code.
8
- 2. Finish proportionate implementer self-review and affected verification. A failed quality check calls for available in-scope diagnosis and cleanup under [refactoring discipline](../../product-rd-workflow/references/refactoring-discipline.md#responding-to-quality-gates); preserve behavior and readability, rerun the check, and escalate only a remaining real blocker. Use `testing-strategy` to select tests: changed named test properties require the killing-mutation walk on disposable or restore-guarded resources; when the same contract has two implementations or paths, use differential/equivalence checks with bounded, asserted known differences. Record a concrete applicability or unavailable-evidence reason when a test family does not run. Do not force a full mutation framework or differential suite onto an unrelated change.
8
+ 2. Finish a deep self-review in the main implementing agent, not a subagent — it holds the intent and decisions, and independence comes from the external review — covering every changed branch and failure path, plus privacy, authority and data-loss risk, then affected verification. A failed quality check calls for available in-scope diagnosis and cleanup under [refactoring discipline](../../product-rd-workflow/references/refactoring-discipline.md#responding-to-quality-gates); preserve behavior and readability, rerun the check, and escalate only a remaining real blocker. Use `testing-strategy` to select tests: changed named test properties require the killing-mutation walk on disposable or restore-guarded resources; when the same contract has two implementations or paths, use differential/equivalence checks with bounded, asserted known differences. Record a concrete applicability or unavailable-evidence reason when a test family does not run. Do not force a full mutation framework or differential suite onto an unrelated change.
9
9
  3. Reuse a terminal independent review only when it covers the current candidate and satisfies the applicable owner gate, reviewer independence and required depth. Record the receipt location and its candidate identifier (commit or diff/packet digest), then compare with the current candidate using the owning gate's binding rules; HEAD alone cannot cover uncommitted edits. A different or missing candidate identifier cannot discharge review. A loaded skill, self-review, planned command, unfinished handle or unverified prose claim is not that evidence. A native subagent result counts only when the owning gate accepts its independence and evidence; it never silently substitutes for a required CLI receipt.
10
- 4. An explicit user instruction to skip review controls this task: record `skipped`, not `passed`, and preserve any separate landing restrictions. Record its original wording and current scope; a superseded or unrelated instruction is not a skip for this task. Do not ask for confirmation of ordinary review already within the authorized development task. User client restrictions and existing confidentiality boundaries still control reviewer selection; capability matters, not a numeric CLI or skill version.
10
+ 4. Self-review and the external review below are yours to run; neither waits on a person. An ordinary change needs no human reviewer, sign-off or risk owner; human sign-off applies only where the product-rd design gate or the safety rules require it for a merge, launch or destructive action.
11
+ 5. An explicit user instruction to skip review controls this task: record `skipped`, not `passed`, and preserve any separate landing restrictions. Record its original wording and current scope; a superseded or unrelated instruction is not a skip for this task. Do not ask for confirmation of ordinary review already within the authorized development task. User client restrictions and existing confidentiality boundaries still control reviewer selection; capability matters, not a numeric CLI or skill version.
11
12
 
12
13
  ## Invoke and finish
13
14
 
@@ -23,7 +24,7 @@ A review quotes the repository's own tracked contract files for the changed path
23
24
 
24
25
  Walk these before you open a pull or merge request, push to one that is already open, ask for a merge, or report the work done or ready — every time, not only after the first review:
25
26
 
26
- 1. Name the commit or packet the last conclusive review covered and compare it with the current candidate: HEAD plus staged, unstaged and untracked implementation files. For a review whose candidate `--base` derived from the whole worktree, the controller records that commit in the worktree's git directory (`ccl-code-review/last-review.json`; a bare `--diff-file` or `--paths` review records nothing), and the plugin's pull-request hook repeats this comparison when you open, ready or merge one; its reminder is this step firing, not a new question.
27
+ 1. Name the commit or packet the last conclusive review covered and compare it with the current candidate: HEAD plus staged, unstaged and untracked implementation files. For a review whose candidate `--base` derived from the whole worktree, the controller records that commit in the worktree's git directory (`ccl-code-review/last-review.json`; a bare `--diff-file` or `--paths` review records nothing), and the plugin's pull-request hook repeats this comparison when you open, ready or merge one; its reminder is this step firing, not a new question. A successful completion checkpoint on such a candidate records it as well, so a chain whose findings were disposed reads as passed; coverage alone is not disposition, and a covered receipt with findings or no status must still be dispositioned before a ready report.
27
28
  2. Any difference makes the current candidate unreviewed: a fix for a finding of any severity, an added or changed test, a changelog or doc line, a rebase that changed a file the candidate touches. Only the review's own record files (result JSON, disposition notes) are exempt.
28
29
  3. An unreviewed candidate must get the owning gate's renewed review now, run by you: a fresh full run per [manual invocation](manual-invocation-and-prompts.md); the skill-extraction lane runs its own delta pass instead. Pushing it for a human to review does not discharge it, and "awaiting human review" never stands in for the run.
29
30
  4. Apply the [review continuation checkpoint](#review-continuation-checkpoint) after five renewed runs or earlier when findings recur without progress. Necessary review continues within the task's authority; unresolved P0/P1 or unreviewed changes still prevent a ready report.
@@ -10,7 +10,8 @@ Rules:
10
10
  their arguments, returned bytes and completed lifecycle. This does not permit
11
11
  arbitrary commands or workspace access. A malformed, timeout, or inconclusive
12
12
  wrapper result is not a pass.
13
- - **Never pin the parser to a CLI version's vocabulary.** `parse_probe_result.py`
13
+ - **Never pin a lane to a CLI version's vocabulary — reading it or writing it.**
14
+ `parse_probe_result.py`
14
15
  gates on *shape*, not on field/value names: the isolation proof is the exact
15
16
  `tools` allowlist plus the tool_use scan, which no init field can bypass.
16
17
  Field-name and value whitelists were tried twice (`fast_mode_state`, then
@@ -19,6 +20,15 @@ Rules:
19
20
  or empty-container, and `agents`/`capabilities` are list-of-strings checks
20
21
  with no value vocabulary. Only `permissionMode` stays value-pinned (it widens
21
22
  what the runtime may do with no tool added).
23
+ The write side is the same class and was missed once: config a wrapper
24
+ GENERATES and then submits to the runtime's own validator as an admission
25
+ precondition carries a key that release may not know, and rejecting it turns
26
+ the whole lane off. So a generated setting that a per-invocation environment
27
+ variable already carries is belt, not the gate — on rejection regenerate the
28
+ strict subset without it and revalidate, unconditionally rather than by
29
+ matching the runtime's error wording, which is the same pin relocated. Only a
30
+ setting with no second, independent path to the same property may fail the
31
+ lane closed, and admission must never widen on the retry.
22
32
  - **Skill, command and plugin lists are vocabulary, not a boundary.**
23
33
  `slash_commands`, `terminal_slash_commands`, `skills` and `plugins` may hold
24
34
  any value and are never judged by name, shape, or origin; only `mcp_servers`
@@ -721,7 +721,7 @@ fi
721
721
  # prevention. The cooperative probe below attempts only private-workspace
722
722
  # Read/Glob/Grep canaries and rejects any observed tool exposure.
723
723
  install_packet_only_config() {
724
- "$PYTHON_BIN_PATH" - "$RUNTIME_HOME/config.toml" "$PACKET_DELIVERY" <<'PY'
724
+ "$PYTHON_BIN_PATH" - "$RUNTIME_HOME/config.toml" "$PACKET_DELIVERY" "${1:-guard}" <<'PY'
725
725
  import json
726
726
  import os
727
727
  from pathlib import Path
@@ -730,6 +730,15 @@ import tomllib
730
730
 
731
731
  config_path = Path(sys.argv[1])
732
732
  delivery = sys.argv[2]
733
+ # "guard" writes the watcher table; "omit" leaves it out for a runtime whose
734
+ # own validator does not recognise it. Both are equally isolated: every
735
+ # invocation sets KIMI_CODE_WATCH, which the runtime reads ahead of the config.
736
+ watch_mode = sys.argv[3]
737
+ # A retry regenerates from the previous generation, so the marker is dropped on
738
+ # the way in and re-added below; the output is the same bytes either way.
739
+ generated_marker = (
740
+ "# Generated by code-review; source-home hooks, tools, and permissions are not inherited."
741
+ )
733
742
  source = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
734
743
  lines = source.splitlines(keepends=True)
735
744
  kept = []
@@ -793,6 +802,8 @@ def decoded_assignment_root(line):
793
802
 
794
803
  for line in lines:
795
804
  stripped = line.strip()
805
+ if stripped == generated_marker:
806
+ continue
796
807
  table_root = decoded_table_root(stripped)
797
808
  if table_root is not None:
798
809
  table_allowed = table_root in safe_table_roots
@@ -828,10 +839,19 @@ guard = (
828
839
  "[tools]\n"
829
840
  f"enabled = {json.dumps(enabled_tools, separators=(',', ':'))}\n"
830
841
  )
842
+ if watch_mode == "guard":
843
+ guard += "[watch]\nenabled = false\n"
831
844
  rendered = "".join(kept).rstrip() + guard
832
845
  parsed = tomllib.loads(rendered)
833
846
  allowed_roots = safe_root_keys | safe_table_roots | {"tools"}
834
- if set(parsed) - allowed_roots or parsed.get("tools") != {"enabled": enabled_tools}:
847
+ expected_watch = {"enabled": False} if watch_mode == "guard" else None
848
+ if watch_mode == "guard":
849
+ allowed_roots = allowed_roots | {"watch"}
850
+ if (
851
+ set(parsed) - allowed_roots
852
+ or parsed.get("tools") != {"enabled": enabled_tools}
853
+ or parsed.get("watch") != expected_watch
854
+ ):
835
855
  raise SystemExit("generated packet-only policy failed semantic verification")
836
856
  replacement = config_path.with_name(f".{config_path.name}.code-review.tmp")
837
857
  replacement.write_text(rendered, encoding="utf-8")
@@ -839,14 +859,30 @@ os.chmod(replacement, 0o600)
839
859
  os.replace(replacement, config_path)
840
860
  PY
841
861
  }
842
- install_packet_only_config \
843
- || die_inconclusive kimi_packet_only_config_failed capability_missing true
844
862
  DOCTOR_STDOUT="$RUN_ROOT/doctor.stdout"
845
863
  DOCTOR_STDERR="$RUN_ROOT/doctor.stderr"
846
- KIMI_CODE_HOME="$RUNTIME_HOME" KIMI_DISABLE_TELEMETRY=1 \
847
- timeout --kill-after=1s 15s "$KIMI_BIN_PATH" doctor config "$RUNTIME_HOME/config.toml" \
848
- >"$DOCTOR_STDOUT" 2>"$DOCTOR_STDERR" \
849
- || die_inconclusive kimi_packet_only_config_unrecognized capability_missing true
864
+ validate_packet_only_config() {
865
+ doctor_rc=0
866
+ KIMI_CODE_HOME="$RUNTIME_HOME" KIMI_CODE_WATCH=0 KIMI_DISABLE_TELEMETRY=1 \
867
+ timeout --kill-after=1s 15s "$KIMI_BIN_PATH" doctor config "$RUNTIME_HOME/config.toml" \
868
+ >"$DOCTOR_STDOUT" 2>"$DOCTOR_STDERR" \
869
+ || doctor_rc=$?
870
+ [ "$doctor_rc" -eq 0 ]
871
+ }
872
+ install_packet_only_config guard \
873
+ || die_inconclusive kimi_packet_only_config_failed capability_missing true
874
+ # The watcher table is belt on top of KIMI_CODE_WATCH, which every invocation
875
+ # below sets and which the runtime reads ahead of the config. A runtime whose
876
+ # validator does not know the table must not cost the whole lane, so retry once
877
+ # with a strict subset of the same policy — admission cannot widen, and the
878
+ # retry is unconditional rather than matched against the runtime's error
879
+ # wording, which would be the same release-vocabulary pin in another place.
880
+ if ! validate_packet_only_config; then
881
+ install_packet_only_config omit \
882
+ || die_inconclusive kimi_packet_only_config_failed capability_missing true
883
+ validate_packet_only_config \
884
+ || die_inconclusive kimi_packet_only_config_unrecognized capability_missing true "$doctor_rc"
885
+ fi
850
886
 
851
887
  # Exercise the forbidden built-in tool surface without depending on a version
852
888
  # string. In MCP mode the generated config still exposes the one packet reader,
@@ -866,7 +902,7 @@ PROBE_TIMEOUT="$TIMEOUT"
866
902
  [ "$PROBE_TIMEOUT" -le 60 ] || PROBE_TIMEOUT=60
867
903
  (
868
904
  cd "$RUN_WORKSPACE" || exit 2
869
- KIMI_CODE_HOME="$RUNTIME_HOME" KIMI_DISABLE_TELEMETRY=1 \
905
+ KIMI_CODE_HOME="$RUNTIME_HOME" KIMI_CODE_WATCH=0 KIMI_DISABLE_TELEMETRY=1 \
870
906
  timeout --kill-after=1s "${PROBE_TIMEOUT}s" "$KIMI_BIN_PATH" \
871
907
  --skills-dir "$ACTIVE_SKILLS_DIR" --prompt "$PROBE_PROMPT" \
872
908
  --output-format stream-json
@@ -876,6 +912,24 @@ if [ "$probe_rc" -ne 0 ]; then
876
912
  if grep -qiE 'EMFILE|too many open files' "$PROBE_STDERR"; then
877
913
  die_inconclusive kimi_host_resource_exhausted client_unavailable true "$probe_rc"
878
914
  fi
915
+ # NO SUB-CLASSIFICATION OF PROVIDER PROSE. Splitting this failure into quota
916
+ # and auth reasons was tried and withdrawn: four independent review rounds
917
+ # each broke the predicate on a new message. A digit boundary excluded 4290
918
+ # but not an offset of exactly 429; requiring a status word before the number
919
+ # matched `code` inside `decode`; matching what the message said instead
920
+ # matched `hit` inside `whitelisted` and still missed `credits are exhausted`.
921
+ # That is the shape this directory's contract already names — a predicate over
922
+ # a vocabulary the control does not own — and same-class recurrence is the cue
923
+ # to remove the capability rather than guard it again.
924
+ #
925
+ # Nothing about the gate changes: every one of those reasons was
926
+ # die_inconclusive and cascade-eligible, so the split only ever decided an
927
+ # operator hint, and a wrong hint is worse than none. The contract also
928
+ # forbids putting raw provider text in the payload, so the honest replacement
929
+ # is not a looser regex but a classifier over the structured error the probe
930
+ # already streams, the way the Claude lane's envelope classifier works. That
931
+ # is its own change, against a real sample.
932
+ :
879
933
  die_inconclusive kimi_tool_capability_unverified capability_missing true "$probe_rc"
880
934
  fi
881
935
  # MCP mode enables the packet reader for the later formal transport, but this
@@ -962,14 +1016,14 @@ run_started=$SECONDS
962
1016
  if [ "$PACKET_DELIVERY" = inline ]; then
963
1017
  (
964
1018
  cd "$RUN_WORKSPACE" || exit 2
965
- KIMI_CODE_HOME="$RUNTIME_HOME" KIMI_DISABLE_TELEMETRY=1 \
1019
+ KIMI_CODE_HOME="$RUNTIME_HOME" KIMI_CODE_WATCH=0 KIMI_DISABLE_TELEMETRY=1 \
966
1020
  timeout --kill-after=1s "${FORMAL_TIMEOUT}s" "$KIMI_BIN_PATH" --skills-dir "$ACTIVE_SKILLS_DIR" \
967
1021
  --prompt "$PROMPT" --output-format stream-json
968
1022
  ) >"$EVENTS" 2>"$STDERR_FILE"
969
1023
  else
970
1024
  (
971
1025
  cd "$RUN_WORKSPACE" || exit 2
972
- KIMI_CODE_HOME="$RUNTIME_HOME" KIMI_DISABLE_TELEMETRY=1 \
1026
+ KIMI_CODE_HOME="$RUNTIME_HOME" KIMI_CODE_WATCH=0 KIMI_DISABLE_TELEMETRY=1 \
973
1027
  timeout --kill-after=1s "${FORMAL_TIMEOUT}s" "$KIMI_BIN_PATH" --skills-dir "$ACTIVE_SKILLS_DIR" \
974
1028
  --agent-file "$AGENT_FILE" --prompt "$PROMPT" --output-format stream-json
975
1029
  ) >"$EVENTS" 2>"$STDERR_FILE"
@@ -4661,8 +4661,10 @@ def main(argv: list[str] | None = None) -> int:
4661
4661
  )
4662
4662
  # Only a candidate derived from the whole worktree says what HEAD holds:
4663
4663
  # a bare --diff-file packet or a --paths slice may cover less than HEAD.
4664
+ # A completion checkpoint records too: it is what turns a findings
4665
+ # receipt into a passed one once every finding is disposed.
4664
4666
  anchors_wanted = (
4665
- args.mode in {"review", "challenge"}
4667
+ args.mode in {"review", "challenge", "complete"}
4666
4668
  and bool(args.base)
4667
4669
  and not args.paths
4668
4670
  )
@@ -4831,6 +4833,7 @@ def main(argv: list[str] | None = None) -> int:
4831
4833
  ),
4832
4834
  )
4833
4835
  result.update(completion_metadata)
4836
+ record_local_review(review_anchor, result)
4834
4837
  return emit(result, 0)
4835
4838
  last_reason_code = "no_independent_reviewer_available"
4836
4839
  for client in order: