@ccoalm/ccl-skills 0.18.3 → 0.18.4

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.
@@ -5,6 +5,7 @@ Codex shapes: openai/codex rust-v0.154.0 protocol/models.rs and
5
5
  core/src/tools/context.rs. Unsupported evidence remains unverifiable.
6
6
  """
7
7
  import hashlib
8
+ import importlib.util
8
9
  import json
9
10
  import os
10
11
  from pathlib import Path
@@ -13,6 +14,10 @@ import shlex
13
14
  import stat
14
15
  import sys
15
16
 
17
+ # Hook assets may be installed read-only; importing the optional state helper
18
+ # must not create bytecode beside them.
19
+ sys.dont_write_bytecode = True
20
+
16
21
 
17
22
  class TranscriptTruncated(ValueError):
18
23
  """The bounded scan could not establish complete transcript evidence."""
@@ -80,8 +85,11 @@ def transcript_lines(path):
80
85
  remaining = 16 * 1024 * 1024
81
86
  descriptor = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
82
87
  with os.fdopen(descriptor, 'rb') as stream:
83
- if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
88
+ metadata = os.fstat(stream.fileno())
89
+ if not stat.S_ISREG(metadata.st_mode):
84
90
  raise ValueError('transcript is not a regular file')
91
+ if metadata.st_size > remaining:
92
+ raise TranscriptTruncated()
85
93
  for _ in range(20000):
86
94
  limit = min(remaining, 1024 * 1024)
87
95
  if limit <= 0:
@@ -480,6 +488,54 @@ def machine_artifact(text):
480
488
  return False
481
489
 
482
490
 
491
+ def stop_notice(payload, lane, message):
492
+ """Cap notice attempts only; these markers never establish verification."""
493
+ try:
494
+ # Reuse the installed runtime's owned-directory/no-follow/atomic claim
495
+ # protections. Minimal vendored runtimes may omit this optional helper.
496
+ source = Path(__file__).resolve().with_name('skill-loading.py')
497
+ spec = importlib.util.spec_from_file_location('ccl_stop_state', source)
498
+ module = importlib.util.module_from_spec(spec)
499
+ spec.loader.exec_module(module)
500
+ # Stop checks scan transcript_path even if subagent metadata also
501
+ # carries a separate agent_transcript_path.
502
+ notice_actor = dict(payload)
503
+ notice_actor.pop('agent_transcript_path', None)
504
+ key, path = module.actor(notice_actor)
505
+ info = module.regular_info(path)
506
+ state = module.State(key)
507
+ try:
508
+ if not state.claim_attempt('stop-notice-' + lane, [info.st_dev, info.st_ino]):
509
+ return None
510
+ finally:
511
+ state.close()
512
+ except Exception:
513
+ # Missing identity, a broken optional helper or unsafe/unavailable state
514
+ # must not invent success. This boundary only controls advisory output.
515
+ pass
516
+ return {'systemMessage': message}
517
+
518
+
519
+ def extraction_overflow(payload):
520
+ path = payload.get('transcript_path')
521
+ cwd = payload.get('cwd')
522
+ current = context_transcript(path, cwd if isinstance(cwd, str) else os.getcwd())
523
+ if any(skill in ('skill-extraction-workflow', 'ccl-skills:skill-extraction-workflow')
524
+ for skill in current['requested_skills']):
525
+ # Positive current-context evidence also proves a session invocation.
526
+ # Missing current evidence cannot prove a missing session invocation.
527
+ return None
528
+ return stop_notice(payload, 'extraction-overflow',
529
+ 'Conversation history is too large for the skill workflow check. '
530
+ 'The check is incomplete; this does not block your task.')
531
+
532
+
533
+ def delivery_eligible(summary):
534
+ return (any(skill in ('product-rd-workflow', 'ccl-skills:product-rd-workflow')
535
+ for skill in summary['completed_skills']) or summary['prior_handoff']
536
+ or summary['continuation_contract_visible'])
537
+
538
+
483
539
  def proposed_next(payload):
484
540
  if (not isinstance(payload, dict) or payload.get('hook_event_name') != 'Stop'
485
541
  or payload.get('stop_hook_active') is not False):
@@ -506,11 +562,16 @@ def proposed_next(payload):
506
562
  if not isinstance(path, str) or not path:
507
563
  return None
508
564
  cwd = payload.get('cwd')
509
- summary = transcript(path, cwd if isinstance(cwd, str) else os.getcwd())
510
- eligible = (any(skill in ('product-rd-workflow', 'ccl-skills:product-rd-workflow')
511
- for skill in summary['completed_skills']) or summary['prior_handoff']
512
- or summary['continuation_contract_visible'])
513
- if not eligible:
565
+ cwd = cwd if isinstance(cwd, str) else os.getcwd()
566
+ try:
567
+ summary = transcript(path, cwd)
568
+ except TranscriptTruncated:
569
+ summary = context_transcript(path, cwd)
570
+ if not delivery_eligible(summary):
571
+ # A complete recent context can establish eligibility, but cannot
572
+ # disprove evidence in the omitted session prefix.
573
+ raise
574
+ if not delivery_eligible(summary):
514
575
  return None
515
576
  return {'decision': 'block', 'reason': (
516
577
  'Delivery handoff reminder: repair one proposed-next: line with the next action and scope, '
@@ -538,18 +599,27 @@ def main():
538
599
  'verifiable': False, 'truncated': True, 'prior_handoff': False,
539
600
  'continuation_contract_visible': False}))
540
601
  return 1
541
- elif sys.argv[1] == 'proposed-next':
602
+ elif sys.argv[1] in ('proposed-next', 'extraction-overflow'):
603
+ payload = {}
542
604
  try:
543
605
  raw = sys.stdin.read(2 * 1024 * 1024 + 1)
544
606
  if len(raw) > 2 * 1024 * 1024:
545
607
  raise ValueError('oversized input')
546
- result = proposed_next(json.loads(raw))
608
+ payload = json.loads(raw)
609
+ result = (extraction_overflow(payload) if sys.argv[1] == 'extraction-overflow'
610
+ else proposed_next(payload))
547
611
  if result:
548
612
  print(json.dumps(result))
549
613
  except TranscriptTruncated:
550
- print(json.dumps({'systemMessage': 'Delivery handoff reminder unverified: transcript scan exceeded its bounded limit.'}))
614
+ result = stop_notice(payload, 'handoff-overflow',
615
+ 'Delivery handoff reminder unverified: transcript scan exceeded its bounded limit.')
616
+ if result:
617
+ print(json.dumps(result))
551
618
  except (OSError, ValueError, TypeError, IndexError, AttributeError):
552
- print(json.dumps({'systemMessage': 'Delivery handoff reminder unavailable: input or transcript could not be verified.'}))
619
+ message = ('Skill workflow check incomplete: input or conversation history could not be verified. '
620
+ 'This does not block your task.' if sys.argv[1] == 'extraction-overflow' else
621
+ 'Delivery handoff reminder unavailable: input or transcript could not be verified.')
622
+ print(json.dumps({'systemMessage': message}))
553
623
  return 0
554
624
 
555
625
 
@@ -2,7 +2,8 @@
2
2
  # Optional Stop handoff/continuation recheck, bounded by host stop_hook_active.
3
3
  # It neither establishes product intent nor authorizes a next action. Source
4
4
  # review that exposes the full canonical rule can produce one advisory repair.
5
- # No transcript echo, host configuration reads, or filesystem markers.
5
+ # No transcript echo or host configuration reads. Incomplete-check notices use
6
+ # optional per-actor attempt markers; markers never suppress the actual check.
6
7
  set -u
7
8
  HELPER="$(cd "$(dirname "$0")" && pwd)/host-input.py"
8
9
  if ! command -v python3 >/dev/null 2>&1 || [ ! -r "$HELPER" ]; then
@@ -46,7 +46,10 @@ command -v python3 >/dev/null 2>&1 && [ -r "$HELPER" ] || {
46
46
  CWD=$(printf '%s' "$IN" | jq -r '.cwd // empty' 2>/dev/null)
47
47
  SUMMARY=$(python3 "$HELPER" transcript "$TRANSCRIPT" "${CWD:-$PWD}" 2>/dev/null) || {
48
48
  if printf '%s' "$SUMMARY" | jq -e '.truncated == true' >/dev/null 2>&1; then
49
- jq -nc '{systemMessage:"Conversation history is too large for the skill workflow check. The check is incomplete; this does not block your task."}'
49
+ # A complete recent context may prove invocation despite the old prefix.
50
+ # Otherwise the helper emits this advisory at most once per actor/file.
51
+ printf '%s' "$IN" | python3 "$HELPER" extraction-overflow 2>/dev/null ||
52
+ jq -nc '{systemMessage:"Conversation history is too large for the skill workflow check. The check is incomplete; this does not block your task."}'
50
53
  else
51
54
  jq -nc '{systemMessage:"Skill workflow check incomplete: conversation history could not be read. This does not block your task."}'
52
55
  fi
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """Host boundary regressions; all repositories and transcripts are synthetic."""
3
3
  import importlib.util
4
+ from concurrent.futures import ThreadPoolExecutor
4
5
  import json
5
6
  import os
6
7
  from pathlib import Path
@@ -317,8 +318,170 @@ class HostInputTests(unittest.TestCase):
317
318
  self.assertIn('unverified', result.get('systemMessage', '').lower())
318
319
  self.assertNotIn('decision', result)
319
320
  self.assertNotIn('Synthetic private status', json.dumps(result))
321
+ repeat = self.run_hook('hooks/' + hook, {
322
+ 'session_id': 'limit-' + label, 'cwd': str(self.repo),
323
+ 'transcript_path': path, 'hook_event_name': 'Stop',
324
+ 'stop_hook_active': False, 'last_assistant_message': 'Synthetic private status.'})
325
+ self.assertEqual(repeat, {}, 'unchanged incomplete checks must not repeat notices')
326
+
327
+ def test_long_history_recovers_positive_evidence_after_compaction(self):
328
+ for owner, hook, expected in (
329
+ ('skill-extraction-workflow', 'skill-extraction-gate-stop.sh', {}),
330
+ ('product-rd-workflow', 'proposed-next-stop.sh', {'decision': 'block'})):
331
+ skill = self.repo / 'skills' / owner / 'SKILL.md'
332
+ skill.parent.mkdir(parents=True, exist_ok=True)
333
+ skill.write_text(f'---\nname: {owner}\n---\n# Synthetic owner\n')
334
+ runtime = self.root / owner
335
+ shutil.copytree(ROOT / 'hooks', runtime / 'hooks')
336
+ canonical = runtime / 'skills' / owner / 'SKILL.md'
337
+ canonical.parent.mkdir(parents=True)
338
+ shutil.copy2(skill, canonical)
339
+ hosts = ('claude', 'codex')
340
+ if owner == 'skill-extraction-workflow':
341
+ hosts += ('claude-pending', 'claude-failed')
342
+ for host in hosts:
343
+ with self.subTest(owner=owner, host=host):
344
+ boundaries = {'claude': {'type': 'system', 'subtype': 'compact_boundary'},
345
+ 'codex': {'type': 'compacted', 'payload': {}}}
346
+ loads = {
347
+ 'claude': [
348
+ {'type': 'assistant', 'message': {'content': [{'type': 'tool_use',
349
+ 'id': 'load', 'name': 'Skill', 'input': {'skill': 'ccl-skills:' + owner}}]}},
350
+ {'type': 'user', 'message': {'content': [{'type': 'tool_result',
351
+ 'tool_use_id': 'load', 'content': 'Loaded'}]}}],
352
+ 'codex': [self.call('exec_command', {'cmd': f'cat {skill}'}),
353
+ self.output('Process exited with code 0\nOutput:\n' + skill.read_text())]}
354
+ family = host.split('-')[0]
355
+ invocation = loads[family]
356
+ if host == 'claude-pending':
357
+ invocation = invocation[:1]
358
+ elif host == 'claude-failed':
359
+ invocation[1]['message']['content'][0]['is_error'] = True
360
+ events = [{'type': 'ignored'}] * 20001 + [boundaries[family]] + invocation
361
+ result = self.run_hook(runtime / 'hooks' / hook, {
362
+ 'session_id': owner + host, 'cwd': str(self.repo),
363
+ 'transcript_path': self.transcript(events), 'hook_event_name': 'Stop',
364
+ 'stop_hook_active': False, 'last_assistant_message': 'Checks recorded.'})
365
+ self.assertEqual({k: v for k, v in result.items() if k != 'reason'}, expected)
366
+
367
+ def test_notice_does_not_suppress_checks_or_other_sessions(self):
368
+ path = self.transcript([{'type': 'ignored'}] * 20001)
369
+ agent_path = self.root / 'agent.jsonl'
370
+ agent_path.write_text('{}\n')
371
+ payload = {'session_id': 'first', 'cwd': str(self.repo), 'transcript_path': path,
372
+ 'agent_transcript_path': str(agent_path),
373
+ 'hook_event_name': 'Stop', 'stop_hook_active': False,
374
+ 'last_assistant_message': 'Checks recorded.'}
375
+ for hook in ('skill-extraction-gate-stop.sh', 'proposed-next-stop.sh'):
376
+ with self.subTest(hook=hook):
377
+ self.assertIn('systemMessage', self.run_hook('hooks/' + hook, payload))
378
+ self.assertEqual(self.run_hook('hooks/' + hook, payload), {})
379
+ self.assertIn('systemMessage', self.run_hook('hooks/' + hook,
380
+ dict(payload, session_id='second')))
381
+ self.assertIn('systemMessage', self.run_hook('hooks/' + hook,
382
+ dict(payload, agent_id='sibling')))
383
+ replacement = self.root / 'replacement.jsonl'
384
+ replacement.write_bytes(Path(path).read_bytes())
385
+ replacement.replace(path)
386
+ self.assertIn('systemMessage', self.run_hook('hooks/proposed-next-stop.sh', payload))
387
+ self.assertEqual(self.run_hook('hooks/proposed-next-stop.sh', payload), {})
388
+ with Path(path).open('a') as stream:
389
+ stream.write(json.dumps({'type': 'system', 'subtype': 'compact_boundary'}) + '\n')
390
+ stream.write(json.dumps({'type': 'assistant', 'message': {'content': [
391
+ {'type': 'text', 'text': 'proposed-next: run local checks'}]}}) + '\n')
392
+ self.assertEqual(self.run_hook('hooks/proposed-next-stop.sh', payload).get('decision'), 'block')
393
+ self.assertEqual(self.run_hook('hooks/proposed-next-stop.sh',
394
+ dict(payload, stop_hook_active=True)), {})
395
+
396
+ def test_concurrent_incomplete_notice_is_claimed_once(self):
397
+ payload = {'session_id': 'parallel', 'cwd': str(self.repo),
398
+ 'transcript_path': self.transcript([{'type': 'ignored'}] * 20001),
399
+ 'hook_event_name': 'Stop', 'stop_hook_active': False,
400
+ 'last_assistant_message': 'Checks recorded.'}
401
+ for hook in ('skill-extraction-gate-stop.sh', 'proposed-next-stop.sh'):
402
+ with self.subTest(hook=hook), ThreadPoolExecutor(max_workers=4) as pool:
403
+ results = list(pool.map(lambda _: self.run_hook('hooks/' + hook, payload), range(4)))
404
+ self.assertEqual(sum('systemMessage' in result for result in results), 1)
405
+ self.assertTrue(all('decision' not in result for result in results))
406
+
407
+ def test_unavailable_notice_state_keeps_truthful_output(self):
408
+ payload = {'session_id': 'unsafe-state', 'cwd': str(self.repo),
409
+ 'transcript_path': self.transcript([{'type': 'ignored'}] * 20001),
410
+ 'hook_event_name': 'Stop', 'stop_hook_active': False,
411
+ 'last_assistant_message': 'Checks recorded.'}
412
+ victim = self.root / 'untouched'
413
+ victim.mkdir()
414
+ (self.root / ('ccl-skill-loading-' + str(os.getuid()))).symlink_to(victim)
415
+ for hook in ('skill-extraction-gate-stop.sh', 'proposed-next-stop.sh'):
416
+ for identity in ({}, {'session_id': None}):
417
+ with self.subTest(hook=hook, identity=identity):
418
+ for _ in range(2):
419
+ result = self.run_hook('hooks/' + hook, dict(payload, **identity))
420
+ self.assertIn('systemMessage', result)
421
+ self.assertNotIn('decision', result)
422
+ self.assertEqual(list(victim.iterdir()), [])
423
+ runtime = self.root / 'minimal-hooks'
424
+ runtime.mkdir()
425
+ for name in ('host-input.py', 'proposed-next-stop.sh', 'skill-extraction-gate-stop.sh'):
426
+ shutil.copy2(ROOT / 'hooks' / name, runtime / name)
427
+ for body in (None, 'raise RuntimeError("synthetic-private-detail")\n', 'invalid syntax !\n'):
428
+ helper = runtime / 'skill-loading.py'
429
+ if body is not None:
430
+ helper.write_text(body)
431
+ for hook, text in (('skill-extraction-gate-stop.sh', 'check is incomplete'),
432
+ ('proposed-next-stop.sh', 'unverified: transcript scan')):
433
+ with self.subTest(optional_helper=body, hook=hook):
434
+ for _ in range(2):
435
+ result = self.run_hook(runtime / hook, payload)
436
+ self.assertIn(text, result.get('systemMessage', ''))
437
+ self.assertNotIn('synthetic-private-detail', json.dumps(result))
438
+ self.assertNotIn('decision', result)
439
+
440
+ def test_long_history_without_positive_current_evidence_stays_unknown(self):
441
+ marker = {'type': 'system', 'subtype': 'compact_boundary'}
442
+ request = {'type': 'assistant', 'message': {'content': [{'type': 'tool_use',
443
+ 'id': 'load', 'name': 'Skill', 'input': {'skill': 'ccl-skills:product-rd-workflow'}}]}}
444
+ failed = {'type': 'user', 'message': {'content': [{'type': 'tool_result',
445
+ 'tool_use_id': 'load', 'is_error': True, 'content': 'Failed'}]}}
446
+ success = {'type': 'user', 'message': {'content': [{'type': 'tool_result',
447
+ 'tool_use_id': 'load', 'content': 'Loaded'}]}}
448
+ for label, tail in [('empty-context', [marker]), ('pending-owner', [marker, request]),
449
+ ('failed-owner', [marker, request, failed]),
450
+ ('no-boundary', [request]),
451
+ ('completed-without-boundary', [request, success]),
452
+ ('invalid-context', [marker, None])]:
453
+ for hook in ('skill-extraction-gate-stop.sh', 'proposed-next-stop.sh'):
454
+ with self.subTest(label=label, hook=hook):
455
+ result = self.run_hook('hooks/' + hook, {
456
+ 'session_id': label, 'cwd': str(self.repo),
457
+ 'transcript_path': self.transcript([{'type': 'ignored'}] * 20001 + tail),
458
+ 'hook_event_name': 'Stop', 'stop_hook_active': False,
459
+ 'last_assistant_message': 'Checks recorded.'})
460
+ self.assertIn('systemMessage', result)
461
+ self.assertNotIn('decision', result)
462
+
463
+ def test_large_session_is_rejected_before_parsing_records(self):
464
+ path = self.root / 'large.jsonl'
465
+ with path.open('wb') as stream:
466
+ stream.write(b'{}\n')
467
+ stream.truncate(17 * 1024 * 1024)
468
+ spec = importlib.util.spec_from_file_location('bounded_probe', ROOT / 'hooks/host-input.py')
469
+ module = importlib.util.module_from_spec(spec)
470
+ spec.loader.exec_module(module)
471
+ with patch.object(module.json, 'loads', side_effect=AssertionError('record parsed')):
472
+ with self.assertRaises(module.TranscriptTruncated):
473
+ module.transcript(str(path), str(self.repo))
320
474
 
321
475
  def test_extraction_helper_failures_explain_task_impact_without_private_details(self):
476
+ for invalid in ('null', '{', '[]'):
477
+ with self.subTest(input=invalid):
478
+ result = subprocess.run(['python3', str(ROOT / 'hooks/host-input.py'),
479
+ 'extraction-overflow'], input=invalid,
480
+ text=True, capture_output=True, env=self.env)
481
+ self.assertEqual(result.returncode, 0, result.stderr)
482
+ message = json.loads(result.stdout)['systemMessage']
483
+ self.assertIn('Skill workflow check incomplete', message)
484
+ self.assertNotIn('Delivery handoff', message)
322
485
  runtime = self.root / 'runtime'
323
486
  runtime.mkdir()
324
487
  hook = runtime / 'skill-extraction-gate-stop.sh'
@@ -708,3 +708,4 @@ The pending classification above is superseded by the executed source comparison
708
708
  | Extraction cadence requires candidate-derived ownership | `code-review` | result-class: failure; behavioral-evidence: RED-baseline; observed-failure: yes; firing-path: command:skills/code-review/scripts/review_gate.py | updated | Owner key `code-review/SKILL.md`. An ordinary source candidate could select the extraction lane by flag while declaring the owner in its plan. The controller now requires its existing path-derived owner evidence before reviewer selection; it never adds the owner solely because the caller selected the lane. |
709
709
  | Ordinary candidates cannot select extraction review or challenge | `skill-extraction-workflow` | result-class: failure; behavioral-evidence: RED-baseline; observed-failure: yes; firing-path: command:skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh | updated | Owner key `skill-extraction-workflow/SKILL.md`. A real-controller negative fixture fails before the ownership repair and passes afterward for both extraction modes, while ordinary staged review still reaches reviewer selection. The declared extraction owner in the fixture plan cannot substitute for candidate-derived evidence. |
710
710
  | Blocker and waiting-status handoffs do not request continuation | `product-rd-workflow` | result-class: failure; behavioral-evidence: RED-baseline; observed-failure: yes; firing-path: file:skills/product-rd-workflow/references/pre-final-continuation-gate.md#Do not request continuation for | updated | Owner key `product-rd-workflow/SKILL.md`. Native-shaped fixtures expose the extra reminder for a concrete blocked handoff or a none marker with a waiting explanation. These statuses now remain non-actionable; a separate action marker and action words that merely start with none still receive the bounded recheck. |
711
+ | Long-history Stop checks suppress repeated incomplete notices and recover positive current-context evidence | `skill-extraction-workflow` / `product-rd-workflow` / `testing-strategy` | result-class: failure; behavioral-evidence: RED-baseline; observed-failure: yes; firing-path: command:hooks/skill-extraction-gate-stop.sh | updated | `specs/146-bounded-hook-history/plan.md` binds repeated overflow, post-compaction recovery and early size-rejection cases. The strict whole-session audit remains unchanged; complete current-context evidence may prove invocation or handoff eligibility, while absence remains unknown. Atomic notice claims are isolated by session, actor and transcript identity and never suppress checks. Existing owner entrypoints remain unchanged; the executable prevention is in the shared hook helper and its subprocess regressions. |
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "npmPackage": "@ccoalm/ccl-skills",
4
- "version": "0.18.3",
5
- "sourceCommit": "29c5c6b6d0e35572de2c269ef75b3680d0b9da35",
4
+ "version": "0.18.4",
5
+ "sourceCommit": "c2455b6c9bde93b197c80139c6fa478e949a249c",
6
6
  "sourceState": "clean",
7
7
  "files": [
8
8
  {
@@ -82,7 +82,7 @@
82
82
  },
83
83
  {
84
84
  "path": "marketplace/plugins/ccl-skills/hooks/host-input.py",
85
- "sha256": "546abe258e25d8103f42259a58b61142418d6c4fd1696f79cfef0fe3876a90c6",
85
+ "sha256": "a124c55a9ad0f24f46cf3745fed1a0bfb2fdbfb4e0a12b0acbbf8d1dfc413ba5",
86
86
  "mode": 420
87
87
  },
88
88
  {
@@ -102,7 +102,7 @@
102
102
  },
103
103
  {
104
104
  "path": "marketplace/plugins/ccl-skills/hooks/proposed-next-stop.sh",
105
- "sha256": "150d6a6a2b87dec330b8affaba5497ecf40483304d52c503d8ebfe6a5e51f2f8",
105
+ "sha256": "f6a37016a9059d3f7d856d177cb2c02242ff4777fcddea1eadd665a5cf8f6161",
106
106
  "mode": 493
107
107
  },
108
108
  {
@@ -142,7 +142,7 @@
142
142
  },
143
143
  {
144
144
  "path": "marketplace/plugins/ccl-skills/hooks/skill-extraction-gate-stop.sh",
145
- "sha256": "b4e5c96cd44d3dd9fdae3d6acd7ac7dc629b1ef52d85c6d42934bc84868fa8bd",
145
+ "sha256": "082eef024bde4dcd842542e398e16b44e6dc5871d4fef168c49366dae020f4fe",
146
146
  "mode": 493
147
147
  },
148
148
  {
@@ -177,7 +177,7 @@
177
177
  },
178
178
  {
179
179
  "path": "marketplace/plugins/ccl-skills/hooks/test_host_input.py",
180
- "sha256": "459cc71337e1d2ddcd125652fe1cb7ed04bda2c65d0d60dcae1d145b1c78dd48",
180
+ "sha256": "d29cd505d0551828b1c91db8490297a48ce44801e6ec6ba998c34a5b7270fa7b",
181
181
  "mode": 420
182
182
  },
183
183
  {
@@ -2207,7 +2207,7 @@
2207
2207
  },
2208
2208
  {
2209
2209
  "path": "marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md",
2210
- "sha256": "a7e0ec62d3670e802ca2ede7d73f796393f657d080a40a34df97c35458490bbc",
2210
+ "sha256": "948afdd0f0a84ce0cdc8a451e6833810a33f970658dda0ce67301c25ba614181",
2211
2211
  "mode": 420
2212
2212
  },
2213
2213
  {
@@ -3518,5 +3518,5 @@
3518
3518
  "mode": 420
3519
3519
  }
3520
3520
  ],
3521
- "snapshotHash": "1f51372beb4f07851dd24d912a7e64042ccfa3bf9a76624cca9857ddf31dcac0"
3521
+ "snapshotHash": "6fc5965309e0fb84b2392f6c57e9eeafa87d640f213dd32a0383d1af7ccaebe9"
3522
3522
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccoalm/ccl-skills",
3
- "version": "0.18.3",
3
+ "version": "0.18.4",
4
4
  "description": "Reusable workflows that help coding agents plan, build, test, review, and release software — for Claude Code, Codex, and OpenCode",
5
5
  "keywords": ["skills", "agent-skills", "claude", "claude-code", "codex", "opencode", "agent", "ai", "ai-agents", "cli", "anthropic", "developer-tools"],
6
6
  "type": "module",