@ccoalm/ccl-skills 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_envelope.py +34 -3
  2. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_abort_leak_state_helpers.sh +148 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_classify_envelope.sh +28 -0
  4. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_review_json.sh +7 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +13 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate_abort_leak.sh +271 -34
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +8 -8
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +7 -8
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +3 -2
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/harness-patterns-and-eval.md +7 -6
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/recurring-anti-patterns-checklist.md +18 -0
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +29 -2
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +28 -29
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +153 -0
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-health.rb +23 -10
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +303 -1
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +222 -91
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +6 -0
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_dateless_host.sh +6 -1
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_verdict_differential.sh +1 -1
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_round_attribution.sh +12 -12
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +455 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_source_refuted.sh +20 -20
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_liveness_predicate_gate.sh +288 -0
  25. package/dist/assets/release.json +39 -24
  26. package/package.json +1 -1
@@ -299,7 +299,11 @@ end
299
299
  # changed owner at all, so the entire row evaluation was skipped and the
300
300
  # surviving row was never even looked at. With no ledger change there are no
301
301
  # added rows, so the skip stays exactly as cheap as before for unrelated diffs.
302
- if upstream.any? || changed_paths.include?(LEDGER_PATH)
302
+ # The routing task bank joins the entry condition (045): a commit that moves only
303
+ # `eval/routing-tasks.jsonl` changes no owner package and need not touch the
304
+ # ledger, so without this the whole block — including the bank-evidence
305
+ # obligation written for exactly that change — was never entered.
306
+ if upstream.any? || changed_paths.include?(LEDGER_PATH) || changed_paths.include?("eval/routing-tasks.jsonl")
303
307
  # Rows are collected PER ROUND and carry the scope they were authored against,
304
308
  # so the classifiers below judge a row against its own round's diff. Reading the
305
309
  # cumulative register diff instead would re-judge every earlier round's rows
@@ -1338,4 +1342,302 @@ if upstream.any? || changed_paths.include?(LEDGER_PATH)
1338
1342
  behavior_failures.each { |path| warn " incomplete: #{path}" }
1339
1343
  exit 1
1340
1344
  end
1345
+
1346
+ # ---- 045: an obligation whose skip leaves no artifact is not enforced ------
1347
+ # Round 044 withdrew several overclaims and, in the same move, demoted the
1348
+ # obligations riding on them into advisories. Five consecutive challenge rounds
1349
+ # returned one shape: the entry condition is stated by the party the obligation
1350
+ # constrains, and SKIPPING IT PRODUCES NOTHING for a reviewer to notice missing.
1351
+ # A reviewer cannot refuse an absent claim.
1352
+ #
1353
+ # Both checks below key on facts of the DIFF — which file changed, which key a
1354
+ # row carries — never on prose semantics. That is deliberate and is the whole
1355
+ # difference from the proxy predicates this repository has repeatedly watched
1356
+ # get walked through: a proxy asks "does this text look like it did the thing",
1357
+ # an invariant asks "is the artifact there". Skipping either obligation now
1358
+ # leaves a detectable absence, which is the half the demotion removed.
1359
+ #
1360
+ # Scope limit, stated here so a later round does not read more into it: these
1361
+ # close OMISSION, not MISCLASSIFICATION. `result-class` is still chosen by the
1362
+ # author, and a bank-evidence locator is not proof the bank actually ran. What
1363
+ # changes is that a silent skip becomes a visible claim the review can accept
1364
+ # or refuse — which is exactly what round 044's reviewer said was missing.
1365
+ fragment_value = lambda do |text, key|
1366
+ text.to_s.split(";").each do |fragment|
1367
+ m = fragment.match(/\A\s*#{Regexp.escape(key)}:\s*(.+?)\s*\z/i)
1368
+ return m[1] if m
1369
+ end
1370
+ nil
1371
+ end
1372
+ declaring_row = lambda do |row|
1373
+ row[:behavior].to_s.split(";").any? { |f| f.match?(/\A\s*behavioral-evidence:/i) }
1374
+ end
1375
+
1376
+ result_classes = %w[failure stable-success insufficient-evidence]
1377
+ # A round is held only to the grammar ITS OWN HEAD declares. Adding a required
1378
+ # field would otherwise refuse every historical round the moment anyone replays
1379
+ # it — which the verdict-differential suite correctly reports as a regression,
1380
+ # and which would make the row grammar permanently unextendable. This is not a
1381
+ # date or commit-id grandfather clause (those are proxies that drift): the
1382
+ # predicate is whether the ledger at that round's head defines the field, so a
1383
+ # round that was never told cannot be refused for not knowing.
1384
+ grammar_marker = "result-class: failure|stable-success|insufficient-evidence"
1385
+ grammar_declared_cache = {}
1386
+ grammar_declared_at = lambda do |ref|
1387
+ grammar_declared_cache[ref] ||= begin
1388
+ bytes = raw_blob_at.call(ref, LEDGER_PATH)
1389
+ next false unless bytes
1390
+ # The DECLARATION is prose, and only prose declares. Scanning the whole blob
1391
+ # let a round delete the declaring paragraph while dropping the same literal
1392
+ # into an ordinary ledger ROW: both sides then read as declared, the
1393
+ # withdrawal check stays empty, and every later round keeps being judged by
1394
+ # a grammar the authority no longer states. Rows quote and describe; they do
1395
+ # not legislate, so a marker sitting inside one is not a declaration.
1396
+ bytes.dup.force_encoding(Encoding::UTF_8).each_line.any? do |line|
1397
+ !line.lstrip.start_with?("|") && line.include?(grammar_marker)
1398
+ end
1399
+ end
1400
+ end
1401
+ # Scoped to rows that resolve to an owner THIS round changed — the same subject
1402
+ # set every other per-row check uses. Walking `rows` instead swept in every
1403
+ # historical ledger line whenever the round's base did not already contain it
1404
+ # (a shallow fixture base, a branch cut far back), turning an append-only ledger
1405
+ # into something that fails on its own past. The obligation is on the round
1406
+ # being judged, not on the file it appends to.
1407
+ # Declared -> undeclared is not a legitimate history, it is the off switch.
1408
+ # Without this, deleting or rewording the paragraph makes both checks skip and
1409
+ # the round is indistinguishable from one that predates them — a silent
1410
+ # disarm wearing the shape of the very non-retroactivity rule that permits it.
1411
+ # Withdrawing the grammar deliberately is still possible; it just cannot be
1412
+ # done quietly, which is the whole subject of this round.
1413
+ grammar_withdrawn = round_bounds.select do |span_base, span_head|
1414
+ grammar_declared_at.call(span_base) && !grammar_declared_at.call(span_head)
1415
+ end
1416
+ unless grammar_withdrawn.empty?
1417
+ warn "impact_chain_grammar_withdrawn: a round removed the ledger's declaration-fragment paragraph, which would disable `result-class` and `bank-evidence` for every later round"
1418
+ warn " note: the checks are gated on the head-declared grammar so history is not judged by rules it was never told. That gate reads an ABSENT declaration as `before the rule`; a declaration that was present and is now gone is a withdrawal, and it has to be argued rather than committed"
1419
+ # Name only a remediation this gate actually implements. An earlier version of
1420
+ # this line offered "land the withdrawal with a named risk owner", and no code
1421
+ # here parses any such artifact — the refusal was unconditional, so the
1422
+ # message promised a way through that did not exist and the round had none.
1423
+ # The escape is deliberately NOT built: a data-side waiver token for a
1424
+ # mechanical obligation is the self-adjudication surface this round removes.
1425
+ # Withdrawing the obligation means removing the triggers, which is a reviewable
1426
+ # code diff rather than a line someone writes about themselves.
1427
+ warn " fix: restore the `result-class: failure|stable-success|insufficient-evidence` definition in #{LEDGER_PATH}. There is no data-side waiver: to retire the obligation, remove the checks in this file in their own reviewed change"
1428
+ grammar_withdrawn.each { |base, head| warn " round: #{base}..#{head}" }
1429
+ exit 1
1430
+ end
1431
+
1432
+ # EVERY declaring row of a round whose head declares the grammar, not only rows
1433
+ # that resolve to a changed owner. Scoping by owner was the first attempt at
1434
+ # non-retroactivity, and it left its own hole: a round that changes only the
1435
+ # routing bank has no changed owner, so a declaring row with no `result-class`
1436
+ # was never examined at all. Non-retroactivity is the grammar gate's job and it
1437
+ # does that job alone; making the owner set do it too bought nothing and cost a
1438
+ # false green. A row that declares an impact chain owes its class, whatever the
1439
+ # round happened to touch.
1440
+ result_class_failures = rows.select do |row|
1441
+ next false unless declaring_row.call(row)
1442
+ next false unless grammar_declared_at.call(row[:scope].head)
1443
+ value = fragment_value.call(row[:behavior], "result-class")
1444
+ !(value && result_classes.include?(value.downcase))
1445
+ end.uniq { |row| row[:line] }
1446
+ unless result_class_failures.empty?
1447
+ warn "impact_chain_result_class_missing: an added row declaring behavioral-evidence carries no usable `result-class`"
1448
+ warn " note: omitting the cell was the cheapest of the three bypasses review found — static checks pass, nothing emits `interim`, and the reviewer has no field to accept or refuse. The value stays the author's call; its ABSENCE no longer does"
1449
+ warn " fix: add `result-class: #{result_classes.join('|')}` to the row's declaration fragments"
1450
+ result_class_failures.each { |row| warn " row: #{row[:line].to_s.sub(/^\+/, '').strip}" }
1451
+ exit 1
1452
+ end
1453
+
1454
+ # Trigger A: a change to the routing surface owes bank evidence.
1455
+ bank_relative = "eval/routing-tasks.jsonl"
1456
+ description_entry_at = lambda do |ref, relative|
1457
+ bytes = raw_blob_at.call(ref, relative)
1458
+ next nil if bytes.nil?
1459
+ parts = split_frontmatter.call(bytes.dup.force_encoding(Encoding::UTF_8))
1460
+ next nil unless parts
1461
+ entry = split_description_entry.call(parts[0])
1462
+ entry && entry[1]
1463
+ end
1464
+ # `upstream` keys are owner-relative (`<owner>/SKILL.md`) while changed_paths and
1465
+ # blob lookups are repo-relative; the same `skills/` prefix the evidence-file
1466
+ # check applies. Without it the description branch silently never fired and the
1467
+ # two cases that DO carry evidence passed for the wrong reason — a false green
1468
+ # that only the paired no-evidence cases could expose.
1469
+ # Owner-scoped ONLY. An earlier version also returned true here whenever the bank
1470
+ # had changed, on the reasoning that the bank is everyone's routing surface. In a
1471
+ # multi-round range that conflated two different subjects: the round that moved
1472
+ # the bank then demanded an owner-specific row from every owner the RANGE had
1473
+ # touched, including owners changed in some other round for unrelated reasons —
1474
+ # so adding a later, unrelated commit changed the verdict on an earlier one. The
1475
+ # bank is its own subject and is checked as one; this predicate answers only
1476
+ # "did THIS owner move ITS OWN description in THIS round".
1477
+ description_touched = lambda do |scope, path|
1478
+ relative = "skills/#{path}"
1479
+ next false unless scope.changed_paths.include?(relative)
1480
+ base_entry = description_entry_at.call(scope.base, relative)
1481
+ head_entry = description_entry_at.call(scope.head, relative)
1482
+ # "No head entry" is two different things and only one of them is exempt.
1483
+ # The FILE being gone (deleted, renamed away) is exempt: its description did
1484
+ # not move, the file did, and the rename machinery owns that case. The file
1485
+ # still being there with its description entry REMOVED is the opposite — the
1486
+ # skill stops advertising what reaches it, which is a routing-surface change
1487
+ # as much as rewording one. Collapsing both into `head_entry.nil?` let a
1488
+ # description be deleted in place with no evidence and no refusal.
1489
+ if head_entry.nil?
1490
+ next false if raw_blob_at.call(scope.head, relative).nil?
1491
+ next !base_entry.nil?
1492
+ end
1493
+ # A path with a head entry and NO base entry is a routing surface being
1494
+ # CREATED — a first description decides which requests reach a new skill, so
1495
+ # it is the largest routing-surface change there is, not an exemption. An
1496
+ # earlier version skipped it as "conservative"; review named that as the hole
1497
+ # it is. The one creation that is not a new routing surface is a rename's
1498
+ # target, where the same description arrives under a new path — git proves
1499
+ # that pairing, so it is excluded on evidence rather than on shape.
1500
+ if base_entry.nil?
1501
+ pair = scope.rename_pairs.find { |_from, to| "#{to}/SKILL.md" == path }
1502
+ if pair
1503
+ # A rename is excluded because the SAME description arrived under a new
1504
+ # path. Excluding the target without checking that made rename+reword in
1505
+ # one commit a free routing change: the target is exempt as "just a
1506
+ # rename" and the source is exempt for having no head entry, so the edit
1507
+ # rides in between them. Compare across the pair — the exemption is for
1508
+ # a description that did not change, not for a path that moved.
1509
+ from_entry = description_entry_at.call(scope.base, "skills/#{pair[0]}/SKILL.md")
1510
+ next true if from_entry.nil?
1511
+ next from_entry != head_entry
1512
+ end
1513
+ next true
1514
+ end
1515
+ base_entry != head_entry
1516
+ end
1517
+ # A downscope is allowed — skipping the measurement is sometimes right — but it
1518
+ # must land somewhere a reader reaches. Requiring the token in a changed spec
1519
+ # keeps `downscoped:` from becoming the next self-adjudicated exit: the author
1520
+ # still decides, and still has to write the decision down where review sees it.
1521
+ downscope_recorded = lambda do |scope, token|
1522
+ # The token has to be a TOKEN. `\S+` accepted `-`, and a substring search then
1523
+ # found it in the bullet punctuation of any changed spec line — so
1524
+ # `downscoped:-` discharged the obligation against a record that says nothing.
1525
+ # A downscope is a decision someone has to be able to look up; a character
1526
+ # that occurs incidentally in prose cannot be that. Eight or more, starting
1527
+ # alphanumeric, and no whitespace: long enough not to collide by accident,
1528
+ # loose enough not to dictate a naming scheme.
1529
+ next false unless token.match?(/\A[A-Za-z0-9][A-Za-z0-9._-]{7,}\z/)
1530
+ # The spec must record the DECLARATION, verbatim — `downscoped:<token>` — not
1531
+ # merely contain the token somewhere. Searching for the bare token let a
1532
+ # common word discharge the obligation against a line that happens to use it
1533
+ # ("Bank evidence anchor …" satisfied `downscoped:evidence`), which is the
1534
+ # silent self-adjudication this gate exists to close, rebuilt out of substring
1535
+ # matching. Requiring the full spelling makes the recorded line say what it is.
1536
+ # Bounded, not substring. `include?("downscoped:TOK")` also matched a spec that
1537
+ # records `downscoped:TOKLONGER` — a strict prefix of somebody else's token
1538
+ # discharging this row — and matched `not-downscoped:TOK` too, which records
1539
+ # the opposite. The declaration has to end where the row's token ends and to
1540
+ # start at the keyword.
1541
+ needle = /(?<![A-Za-z0-9._-])downscoped:#{Regexp.escape(token)}(?![A-Za-z0-9._-])/
1542
+ scope.changed_paths.any? do |relative|
1543
+ next false unless relative.start_with?("specs/") && relative.end_with?(".md")
1544
+ added_lines_for.call(scope, relative).any? { |line| line.match?(needle) }
1545
+ end
1546
+ end
1547
+
1548
+ # Evaluated PER ROUND, not per owner. Two defects independent review found in
1549
+ # the per-owner shape, both of which let the obligation evaporate:
1550
+ # - a diff that changes ONLY `eval/routing-tasks.jsonl` has no changed owner,
1551
+ # so the per-owner loop was never entered and the routing bank could move
1552
+ # with no evidence at all. The first fixture missed it by also touching an
1553
+ # owner, which is how a hole hides behind a green case.
1554
+ # - `triggered` and `satisfied` each scanned every row of the owner across ALL
1555
+ # rounds, so one round's valid locator satisfied a different round's naked
1556
+ # routing-surface change. The obligation belongs to the round that incurred
1557
+ # it; the row that discharges it has to sit in the same round.
1558
+ bank_evidence_failures = []
1559
+ # Iterating the ROUNDS, not the rows: grouping by row scope meant a round that
1560
+ # moved the bank and appended no ledger row had no scope to inspect, so the one
1561
+ # change that needs no owner and no row escaped entirely. The first fixture for
1562
+ # this hid it by also appending a row — twice now a green case has stood in
1563
+ # front of the hole it was written to cover.
1564
+ round_bounds.each do |span_base, span_head|
1565
+ scope = scope_at.call(span_base, span_head)
1566
+ scope_rows = rows.select { |row| row[:scope].head == span_head }
1567
+ # Same head-declared-grammar rule as `result-class` above, and for the same
1568
+ # reason: the paragraph that defines `bank-evidence` also dates it.
1569
+ next unless grammar_declared_at.call(scope.head)
1570
+ bank_changed = scope.changed_paths.include?(bank_relative)
1571
+ triggered_owners = (upstream + lineage_extra.keys).uniq.select do |path|
1572
+ description_touched.call(scope, path)
1573
+ end
1574
+ # A package that did not exist at the round's base is not in the owner set —
1575
+ # the set is built from CHANGED owners, and a brand-new skill has nothing to
1576
+ # have changed. Its first description is still a routing surface being
1577
+ # created, so it is picked up from the changed paths directly rather than
1578
+ # being silently outside the trigger because of how the owner set is built.
1579
+ created_surfaces = scope.changed_paths.filter_map do |relative|
1580
+ next unless relative.start_with?("skills/") && relative.end_with?("/SKILL.md")
1581
+ path = relative.sub(%r{\Askills/}, "")
1582
+ next if triggered_owners.include?(path)
1583
+ next unless description_touched.call(scope, path)
1584
+ path
1585
+ end
1586
+ triggered_owners |= created_surfaces
1587
+ next if !bank_changed && triggered_owners.empty?
1588
+ # Discharged PER SUBJECT, not per round. Scanning the whole round meant that
1589
+ # when two owners changed their descriptions, one owner's valid locator
1590
+ # marked the round satisfied and the other could omit its evidence entirely —
1591
+ # the obligation is incurred by each owner that moved its routing surface, so
1592
+ # each has to carry its own.
1593
+ row_satisfies = lambda do |row, self_owner|
1594
+ value = fragment_value.call(row[:behavior], "bank-evidence")
1595
+ next false unless value
1596
+ if (m = value.match(/\Adownscoped:(\S+)\z/i))
1597
+ downscope_recorded.call(scope, m[1])
1598
+ else
1599
+ # A locator pointing back into the owner's own package is the change, not
1600
+ # evidence about it. Without this a description edit satisfies its own
1601
+ # bank obligation by citing the very line it just wrote — an artifact that
1602
+ # exists and proves nothing, which is the shape this whole round removes.
1603
+ # Checked on the PATH after stripping the kind prefix, not on the `file:`
1604
+ # spelling: the first version matched `file:skills/<owner>/` only, so an
1605
+ # owner that also touched a script in its own package could cite it as
1606
+ # `command:skills/<owner>/scripts/…` and validate the change as evidence
1607
+ # about itself. Both locator forms name a path; the ban is about the path.
1608
+ if self_owner
1609
+ located = value.sub(/\A(?:command|file):/i, "").sub(/#.*\z/m, "")
1610
+ # Normalize before the prefix test. A raw `start_with?` compares one
1611
+ # spelling of a path, and `./skills/<owner>/…` or a `a/../` detour names
1612
+ # the same file while missing the check — the ban is on the LOCATION, so
1613
+ # it has to be decided on a canonical form of it.
1614
+ segments = []
1615
+ located.split("/").each do |seg|
1616
+ next if seg.empty? || seg == "."
1617
+ seg == ".." ? segments.pop : segments << seg
1618
+ end
1619
+ next false if segments.take(2) == ["skills", self_owner]
1620
+ end
1621
+ locator_valid.call(scope, value)
1622
+ end
1623
+ end
1624
+ triggered_owners.each do |path|
1625
+ owner = path.sub(%r{/SKILL\.md\z}, "")
1626
+ owner_scope_rows = rows_by_upstream_path[path].select { |row| row[:scope].head == scope.head }
1627
+ bank_evidence_failures << path unless owner_scope_rows.any? { |row| row_satisfies.call(row, owner) }
1628
+ end
1629
+ if bank_changed
1630
+ # The bank is nobody's package, so any row of this round may discharge it;
1631
+ # what it may not be is absent.
1632
+ bank_evidence_failures << bank_relative unless scope_rows.any? { |row| row_satisfies.call(row, nil) }
1633
+ end
1634
+ end
1635
+ bank_evidence_failures.uniq!
1636
+ unless bank_evidence_failures.empty?
1637
+ warn "impact_chain_bank_evidence_missing: a round that changed the routing surface carries no bank evidence for that owner"
1638
+ warn " note: the routing surface is the SKILL.md frontmatter `description` entry and `#{bank_relative}`. The measurement protocol says it is mandatory, but nothing produced or consumed it, so not running it left no absence to detect — the reviewer saw a candidate, not a gap"
1639
+ warn " fix: add `bank-evidence: command:<changed executable>` or `bank-evidence: file:<changed markdown>#<unique anchor>` to that owner's row; to skip the run deliberately use `bank-evidence: downscoped:<token>` and record the same token in this round's spec, so the downscope is itself an artifact"
1640
+ bank_evidence_failures.each { |path| warn " owes bank evidence: #{path}" }
1641
+ exit 1
1642
+ end
1341
1643
  end