@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.
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/host-input.py +80 -10
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/proposed-next-stop.sh +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/skill-extraction-gate-stop.sh +4 -1
- package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_host_input.py +163 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +1 -0
- package/dist/assets/release.json +8 -8
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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. |
|
package/dist/assets/release.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"npmPackage": "@ccoalm/ccl-skills",
|
|
4
|
-
"version": "0.18.
|
|
5
|
-
"sourceCommit": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
+
"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",
|