@olegkoval/agent-skills 1.69.4 → 1.71.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 (57) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/.github/prompts/team-retrospective.prompt.md +31 -0
  3. package/.github/prompts/wrap-up.prompt.md +2 -0
  4. package/.kiro/steering/team-retrospective.md +30 -0
  5. package/.kiro/steering/wrap-up.md +2 -0
  6. package/.windsurf/rules/team-retrospective.md +29 -0
  7. package/.windsurf/rules/wrap-up.md +2 -0
  8. package/README.md +1 -0
  9. package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +11 -2
  10. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/conventions.md +3 -0
  11. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/implementation.md +3 -0
  12. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/quality.md +3 -0
  13. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/simplification.md +3 -0
  14. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/test-quality.md +3 -0
  15. package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/verifier.md +28 -3
  16. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/project-rules/CLAUDE.md +4 -0
  17. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/project-rules/planted.diff +9 -0
  18. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/project-rule-findings.mjs +122 -0
  19. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +160 -0
  20. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +63 -5
  21. package/adapters/claude/olko-reflection/skills/team-retrospective/SKILL.md +38 -0
  22. package/adapters/claude/olko-reflection/skills/wrap-up/SKILL.md +2 -0
  23. package/adapters/codex/olko-reflection/README.md +1 -0
  24. package/adapters/cursor/olko-reflection/skills/team-retrospective/SKILL.md +38 -0
  25. package/adapters/cursor/olko-reflection/skills/wrap-up/SKILL.md +2 -0
  26. package/adapters/grok/olko-reflection/skills/team-retrospective/SKILL.md +38 -0
  27. package/adapters/grok/olko-reflection/skills/wrap-up/SKILL.md +2 -0
  28. package/adapters/hermes/olko-reflection/README.md +1 -0
  29. package/catalog/skills.json +38 -2
  30. package/package.json +1 -1
  31. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  32. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  33. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  34. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  35. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  36. package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +11 -2
  37. package/plugins/olko-github-pr/skills/lekker-review/references/agents/conventions.md +3 -0
  38. package/plugins/olko-github-pr/skills/lekker-review/references/agents/implementation.md +3 -0
  39. package/plugins/olko-github-pr/skills/lekker-review/references/agents/quality.md +3 -0
  40. package/plugins/olko-github-pr/skills/lekker-review/references/agents/simplification.md +3 -0
  41. package/plugins/olko-github-pr/skills/lekker-review/references/agents/test-quality.md +3 -0
  42. package/plugins/olko-github-pr/skills/lekker-review/references/agents/verifier.md +28 -3
  43. package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/project-rules/CLAUDE.md +4 -0
  44. package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/project-rules/planted.diff +9 -0
  45. package/plugins/olko-github-pr/skills/lekker-review/scripts/project-rule-findings.mjs +122 -0
  46. package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +160 -0
  47. package/plugins/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +63 -5
  48. package/plugins/olko-github-pr/skills/lekker-review/workflow.js +62 -2
  49. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  50. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  51. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  52. package/plugins/olko-reflection/skills/team-retrospective/SKILL.md +37 -0
  53. package/plugins/olko-reflection/skills/wrap-up/SKILL.md +2 -0
  54. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  55. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  56. package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
  57. package/scripts/lib/catalog.mjs +1 -1
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ // Validates findings tagged `rule: "PROJECT:<slug>"`.
3
+ //
4
+ // A project's own prohibitions and obligations (CLAUDE.md, AGENTS.md, REVIEW.md)
5
+ // are collected into `projectRules`. A lens that raises one as a hard rule must
6
+ // quote the rule it enforces. A quote that is not in `projectRules` means the
7
+ // model invented the rule, so the finding is rejected instead of being allowed
8
+ // to ride the hard-rule path past the runtime challenges.
9
+ //
10
+ // node project-rule-findings.mjs <finding.json> <context.json>
11
+ //
12
+ // Exit 0 when valid or when the finding is not a PROJECT rule, 1 when rejected,
13
+ // 2 when the validator itself could not run (usage error, unreadable or malformed input).
14
+ import { readFileSync, realpathSync } from 'node:fs'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ const TAG_PATTERN = /^PROJECT:[a-z0-9]+(-[a-z0-9]+)*$/
18
+ const MIN_QUOTE_LENGTH = 12
19
+
20
+ export function isProjectRuleTag(tag) {
21
+ return typeof tag === 'string' && TAG_PATTERN.test(tag)
22
+ }
23
+
24
+ // Case, runs of whitespace and markdown emphasis or list markers vary between a
25
+ // rules file and a model's copy of it; the words must not.
26
+ export function normalizeQuote(text) {
27
+ return String(text ?? '')
28
+ .replace(/(^|\n)\s{0,3}#{1,6}\s+/g, '$1')
29
+ .replace(/(^|\n)\s{0,3}>\s?/g, '$1')
30
+ .replace(/`([^`\n]+)`/g, '$1')
31
+ .replace(/(^|[\s([{])(\*{1,2}|_{1,2})(\S(?:.*?\S)?)\2(?=$|[\s.,;:!?)}\]])/g, '$1$3')
32
+ .replace(/^\s*[-+]\s+/gm, ' ')
33
+ .replace(/\s+/g, ' ')
34
+ .trim()
35
+ .toLowerCase()
36
+ }
37
+
38
+ const RECALLED_PATTERNS_HEADING = /^\s{0,3}#{1,6}\s+recalled patterns\s*$/gim
39
+
40
+ // Recalled patterns are memory, not the project's rules, so nothing below the last such heading is a valid quote source.
41
+ // SKILL.md appends that heading at the end; an earlier one inside a rules file must not drop the rules after it.
42
+ export function stripRecalledPatterns(text) {
43
+ const source = String(text ?? '')
44
+ let cut = -1
45
+ for (const match of source.matchAll(RECALLED_PATTERNS_HEADING)) cut = match.index
46
+ return cut === -1 ? source : source.slice(0, cut)
47
+ }
48
+
49
+ export function quoteAppearsIn(quote, projectRules) {
50
+ const needle = normalizeQuote(quote)
51
+ if (!needle) return false
52
+ const rules = stripRecalledPatterns(projectRules)
53
+ if (needle.length >= MIN_QUOTE_LENGTH && normalizeQuote(rules).includes(needle)) return true
54
+ return rules.split(/\r?\n/).some(line => normalizeQuote(line) === needle)
55
+ }
56
+
57
+ export function validateProjectRuleFinding(finding, projectRules) {
58
+ const tag = finding?.rule
59
+ if (typeof tag !== 'string' || !tag.startsWith('PROJECT:')) {
60
+ return { ok: true, reason: 'not a PROJECT rule' }
61
+ }
62
+ if (!isProjectRuleTag(tag)) {
63
+ return { ok: false, reason: `malformed tag "${tag}": expected PROJECT:<kebab-case-slug>` }
64
+ }
65
+ if (typeof finding.ruleQuote !== 'string' || finding.ruleQuote.trim() === '') {
66
+ return { ok: false, reason: 'missing ruleQuote: quote the project rule verbatim' }
67
+ }
68
+ if (!quoteAppearsIn(finding.ruleQuote, projectRules)) {
69
+ return { ok: false, reason: 'ruleQuote not found in projectRules' }
70
+ }
71
+ return { ok: true, reason: 'quote found in projectRules' }
72
+ }
73
+
74
+ export function validateWeakenedProjectRuleFinding(finding, baseProjectRules) {
75
+ if (finding?.rule !== 'weakens-project-rule') {
76
+ return { ok: true, reason: 'not a weakened project rule' }
77
+ }
78
+ if (typeof baseProjectRules !== 'string') {
79
+ return { ok: false, reason: 'baseProjectRules unavailable' }
80
+ }
81
+ if (typeof finding.ruleQuote !== 'string' || finding.ruleQuote.trim() === '') {
82
+ return { ok: false, reason: 'missing ruleQuote: quote the removed project rule' }
83
+ }
84
+ if (!quoteAppearsIn(finding.ruleQuote, baseProjectRules)) {
85
+ return { ok: false, reason: 'ruleQuote not found in baseProjectRules' }
86
+ }
87
+ return { ok: true, reason: 'quote found in baseProjectRules' }
88
+ }
89
+
90
+ function main(argv) {
91
+ const [findingPath, contextPath] = argv
92
+ if (!findingPath || !contextPath) {
93
+ console.error('usage: project-rule-findings.mjs <finding.json> <context.json>')
94
+ return 2
95
+ }
96
+ try {
97
+ const finding = JSON.parse(readFileSync(findingPath, 'utf8'))
98
+ const context = JSON.parse(readFileSync(contextPath, 'utf8'))
99
+ if (finding?.rule === 'weakens-project-rule') {
100
+ const result = validateWeakenedProjectRuleFinding(finding, context.baseProjectRules)
101
+ console.log(JSON.stringify(result))
102
+ return result.ok ? 0 : 1
103
+ }
104
+ // Base-branch rules are authoritative; PR-tree rules only when no base was resolved.
105
+ const authoritative = typeof context.baseProjectRules === 'string' ? context.baseProjectRules : context.projectRules
106
+ const result = validateProjectRuleFinding(finding, authoritative)
107
+ console.log(JSON.stringify(result))
108
+ return result.ok ? 0 : 1
109
+ } catch (error) {
110
+ // Exit 1 is reserved for "rejected"; a tooling failure must not read as one.
111
+ console.error(`project-rule-findings: unavailable: ${error.message}`)
112
+ return 2
113
+ }
114
+ }
115
+
116
+ function isMain() {
117
+ try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)) } catch { return false }
118
+ }
119
+
120
+ if (isMain()) {
121
+ process.exit(main(process.argv.slice(2)))
122
+ }
@@ -696,5 +696,165 @@ check('ledger: proven prover adds one', countProveAttempt(2, { attempted: true,
696
696
  const bareWrites = src.split('\n').filter(l => /proveAttemptCount\s*(\+\+|\+=|=[^=])/.test(l) && !/countProveAttempt\(|let proveAttemptCount/.test(l))
697
697
  check('the attempt count is written only through countProveAttempt', bareWrites, [])
698
698
 
699
+ console.log('\nproject rules: PROJECT:<slug> findings must quote a rule that exists')
700
+ const { spawnSync: spawnPr } = await import('node:child_process')
701
+ const { mkdtempSync: mkPr, cpSync: cpPr, readFileSync: rdPr, rmSync: rmPr, symlinkSync: syPr, writeFileSync: wfPr } = await import('node:fs')
702
+ const { tmpdir: tmpPr } = await import('node:os')
703
+ const { validateProjectRuleFinding, validateWeakenedProjectRuleFinding, isProjectRuleTag } =
704
+ await import('./project-rule-findings.mjs')
705
+
706
+ const fixtureDir = join(SKILL, 'scripts', 'fixtures', 'project-rules')
707
+ const prRepo = mkPr(join(tmpPr(), 'lekker-pr-'))
708
+ cpPr(join(fixtureDir, 'CLAUDE.md'), join(prRepo, 'CLAUDE.md'))
709
+ const prGit = (...a) => spawnPr('git', ['-C', prRepo, '-c', 'user.name=t', '-c', 'user.email=t@t', ...a], { encoding: 'utf8' })
710
+ prGit('init', '-q', '-b', 'main'); prGit('add', '.'); prGit('commit', '-qm', 'rules')
711
+ const prOut = join(prRepo, 'rules.json')
712
+ const collected = spawnPr('bash', [join(SKILL, 'scripts', 'setup-worktree.sh'), 'o/r', 'main', prOut], {
713
+ encoding: 'utf8', env: { ...process.env, LEKKER_RULES_ONLY: '1', LEKKER_LOCAL_REPO: prRepo },
714
+ })
715
+ const planted = collected.status === 0 ? JSON.parse(rdPr(prOut, 'utf8')).projectRules : ''
716
+ check('rules collection exits 0 on the planted fixture', collected.status, 0)
717
+ check('projectRules carries both planted rules',
718
+ [planted.includes('NEVER call console.log in src/.'), planted.includes('NEVER mutate function arguments.')], [true, true])
719
+ rmPr(prRepo, { recursive: true, force: true })
720
+
721
+ const prFinding = (rule, ruleQuote) => ({ rule, ruleQuote, file: 'src/total.js', line: 2, title: 't' })
722
+ check('accepts PROJECT:no-console-log with a matching quote',
723
+ validateProjectRuleFinding(prFinding('PROJECT:no-console-log', 'NEVER call console.log in src/.'), planted).ok, true)
724
+ check('accepts a quote that differs only in case and spacing',
725
+ validateProjectRuleFinding(prFinding('PROJECT:no-mutation', 'never mutate function arguments'), planted).ok, true)
726
+ check('accepts exact short project rules',
727
+ validateProjectRuleFinding(prFinding('PROJECT:no-eval', 'NEVER eval.'), '- NEVER eval.').ok, true)
728
+ check('rejects altered identifiers in project rule quotes',
729
+ validateProjectRuleFinding(prFinding('PROJECT:no-unsafe', 'NEVER call unsafe().'), 'NEVER call `_unsafe()`.').ok, false)
730
+ check('rejects altered operators in project rule quotes',
731
+ validateProjectRuleFinding(prFinding('PROJECT:nonnegative', 'MUST validate n = 0.'), 'MUST validate n >= 0.').ok, false)
732
+ check('rejects a quote that is absent from projectRules',
733
+ validateProjectRuleFinding(prFinding('PROJECT:no-eval', 'NEVER call eval anywhere in the codebase'), planted).ok, false)
734
+ check('rejects a PROJECT tag with no quote',
735
+ validateProjectRuleFinding({ rule: 'PROJECT:no-console-log' }, planted).ok, false)
736
+ const weakenedFinding = prFinding('weakens-project-rule', 'NEVER call console.log in src/.')
737
+ check('accepts a weakened rule quoted from baseProjectRules',
738
+ validateWeakenedProjectRuleFinding(weakenedFinding, planted).ok, true)
739
+ check('rejects a weakened rule quote absent from baseProjectRules',
740
+ validateWeakenedProjectRuleFinding(weakenedFinding, 'NEVER call eval.').ok, false)
741
+ check('rejects a weakened rule when baseProjectRules is unavailable',
742
+ validateWeakenedProjectRuleFinding(weakenedFinding, undefined).ok, false)
743
+ check('rejects a weakened rule with no quote',
744
+ validateWeakenedProjectRuleFinding({ rule: 'weakens-project-rule' }, planted).ok, false)
745
+ const verifierInstructions = rdPr(join(SKILL, 'references', 'agents', 'verifier.md'), 'utf8')
746
+ check('verifier has a dedicated weakened-project-rule path',
747
+ verifierInstructions.includes('For `rule: "weakens-project-rule"`, use this dedicated path:'), true)
748
+ check('verifier accepts deleted and replacement-line anchors',
749
+ verifierInstructions.includes('valid for both a deletion and a replacement'), true)
750
+ check('verifier confirms valid weakened rules at Important severity',
751
+ verifierInstructions.includes("`confirmed` at the finding's Important severity"), true)
752
+ check('rejects a malformed slug', validateProjectRuleFinding(prFinding('PROJECT:No Console', 'NEVER call console.log in src/.'), planted).ok, false)
753
+ check('a built-in tag is left to the house-rule path', validateProjectRuleFinding({ rule: 'TS-1' }, planted).ok, true)
754
+ const withRecalled = 'NEVER eval anywhere in src.\n## Recalled patterns\nNEVER call console.log in tests.'
755
+ check('accepts a quote above the recalled patterns heading',
756
+ validateProjectRuleFinding(prFinding('PROJECT:no-eval', 'NEVER eval anywhere in src.'), withRecalled).ok, true)
757
+ check('rejects a quote that appears only under recalled patterns',
758
+ validateProjectRuleFinding(prFinding('PROJECT:no-console-log', 'NEVER call console.log in tests.'), withRecalled).ok, false)
759
+ const twoRecalled = 'NEVER eval anywhere in src.\n## Recalled patterns\nNEVER use var in src.\n## Recalled patterns\nNEVER call console.log in tests.'
760
+ check('accepts a quote between two recalled patterns headings',
761
+ validateProjectRuleFinding(prFinding('PROJECT:no-var', 'NEVER use var in src.'), twoRecalled).ok, true)
762
+ check('rejects a quote after the last recalled patterns heading when the heading appears twice',
763
+ validateProjectRuleFinding(prFinding('PROJECT:no-console-log', 'NEVER call console.log in tests.'), twoRecalled).ok, false)
764
+ console.log('\nworkflow: a house-rule finding and a PROJECT finding on one line are never one issue')
765
+ const houseF = { file: 'src/total.js', line: 2, title: 'Missing null check on total', rule: 'TS-1' }
766
+ const projectF = { file: 'src/total.js', line: 2, title: 'Missing null check on total', rule: 'PROJECT:no-null-total', ruleQuote: 'NEVER skip the null check on total.' }
767
+ check('a house-rule finding and a PROJECT finding with similar titles are not sameIssue', sameIssue(houseF, projectF), false)
768
+ check('isProjectRuleTag accepts a kebab slug and rejects an empty one',
769
+ [isProjectRuleTag('PROJECT:a-b'), isProjectRuleTag('PROJECT:')], [true, false])
770
+ check('a PROJECT tag is a hard rule', isHardRule({ rule: 'PROJECT:no-console-log' }), true)
771
+ const quoteA = { file: 'src/total.js', line: 2, title: 'Missing null check on total', rule: 'PROJECT:no-null-total', ruleQuote: 'NEVER skip the null check on total.' }
772
+ const quoteB = { ...quoteA, ruleQuote: 'ALWAYS validate total before use.' }
773
+ check('two different quotes of one PROJECT rule are not sameIssue', sameIssue(quoteA, quoteB), false)
774
+ check('the same quote of one PROJECT rule is still sameIssue', sameIssue(quoteA, { ...quoteA }), true)
775
+
776
+ // Prompt guard: every lens that reads projectRules is told to raise the tag.
777
+ const lensFiles = ['quality', 'implementation', 'simplification', 'conventions', 'test-quality']
778
+ check('every lens file that reads projectRules carries the PROJECT instruction',
779
+ lensFiles.filter(n => {
780
+ const t = rdPr(join(SKILL, 'references', 'agents', `${n}.md`), 'utf8')
781
+ return t.includes('"projectRules"') && !t.includes('PROJECT:<slug>')
782
+ }), [])
783
+ // Behavioural: lift the real reviewPrompt and build prompts with it. Removing the
784
+ // append in reviewPrompt makes the listed-lens check fail.
785
+ function liftBlock(name) {
786
+ const m = new RegExp(`^const ${name} = [\\s\\S]*?\\n\\n`, 'm').exec(src)
787
+ if (!m) throw new Error(`selftest: const ${name} not found in workflow.js`)
788
+ return m[0]
789
+ }
790
+ const buildPrompt = new Function(
791
+ 'dim', 'SUPPRESSED', 'prevSha',
792
+ [liftBlock('PROJECT_RULE_DIMENSIONS'), liftBlock('PROJECT_RULE_INSTRUCTION'), liftConst('BUG_CLASS_INSTRUCTION'),
793
+ `const targetMetadata = '{}', promptDir = 'p', repoSlug = 'o/r', prNumber = 1, prUrl = 'u',
794
+ diffFile = 'd', contextFile = 'c', wtDisplay = 'w'`,
795
+ lift('reviewPrompt'), 'return reviewPrompt(dim)'].join('\n'),
796
+ )
797
+ const promptFor = key => buildPrompt({ key, file: `${key}.md` }, [], null)
798
+ check('a listed lens prompt carries the PROJECT instruction',
799
+ lensFiles.map(n => promptFor(n).includes('"PROJECT:<short-kebab-slug>"')), lensFiles.map(() => true))
800
+ check('an unlisted lens prompt does not', promptFor('deps').includes('PROJECT:<short-kebab-slug>'), false)
801
+
802
+ console.log('\nproject rules: dedup keeps the tag and its quote together')
803
+ const { dedup: dedupPr } = new Function(preamble + '\nreturn { dedup }')()
804
+ const plainF = { file: 'src/a.js', line: 2, severity: 'important', title: 'console.log left in src', bugClass: 'console-log', description: 'x', badCode: 'console.log(1)', fix: 'f', otherInstances: [] }
805
+ const taggedF = { ...plainF, severity: 'critical', rule: 'PROJECT:no-console-log', ruleQuote: 'NEVER call console.log in src/.' }
806
+ const taggedWithoutQuote = { ...taggedF }
807
+ delete taggedWithoutQuote.ruleQuote
808
+ for (const [label, pair] of [['plain first', [plainF, taggedF]], ['tagged first', [taggedF, plainF]]]) {
809
+ const [m] = dedupPr(pair)
810
+ check(`merge keeps rule and ruleQuote (${label})`, [m.rule, m.ruleQuote], ['PROJECT:no-console-log', 'NEVER call console.log in src/.'])
811
+ }
812
+ const [mergedQuote] = dedupPr([taggedWithoutQuote, taggedF])
813
+ check('merge fills missing quote for the same project rule',
814
+ [mergedQuote.rule, mergedQuote.ruleQuote], ['PROJECT:no-console-log', 'NEVER call console.log in src/.'])
815
+
816
+ const strayQuote = { ...plainF, ruleQuote: 'stale quote that belongs to no rule' }
817
+ const [mergedStray] = dedupPr([strayQuote, taggedF])
818
+ check('a stray quote on an untagged side does not survive the merge',
819
+ [mergedStray.rule, mergedStray.ruleQuote], ['PROJECT:no-console-log', 'NEVER call console.log in src/.'])
820
+ const [mergedLate] = dedupPr([{ ...plainF, rule: 'PROJECT:no-console-log' }, taggedF])
821
+ check('a quote from a later finding fills a quote-less same rule', mergedLate.ruleQuote, 'NEVER call console.log in src/.')
822
+ const otherRule = { ...taggedF, rule: 'PROJECT:no-mutation', ruleQuote: 'NEVER mutate function arguments.' }
823
+ check('two different PROJECT rules stay separate through dedup', dedupPr([taggedF, otherRule]).length, 2)
824
+ const [mergedNoQuote] = dedupPr([strayQuote, { ...taggedF, ruleQuote: undefined }])
825
+ check('a tagged side without a quote leaves no stray quote behind', mergedNoQuote.ruleQuote, undefined)
826
+
827
+ console.log('\nproject rules: validator crash is exit 2, rejection is exit 1')
828
+ const crashDir = mkPr(join(tmpPr(), 'lekker-crash-'))
829
+ const okCtx = join(crashDir, 'ctx.json'), badJson = join(crashDir, 'bad.json'), fnd = join(crashDir, 'f.json')
830
+ wfPr(okCtx, JSON.stringify({ projectRules: 'NEVER call console.log in src/.' }))
831
+ wfPr(badJson, '{not json')
832
+ wfPr(fnd, JSON.stringify({ rule: 'PROJECT:no-console-log', ruleQuote: 'NEVER call console.log in src/.' }))
833
+ const runV = (f, c) => spawnPr('node', [join(SKILL, 'scripts', 'project-rule-findings.mjs'), f, c], { encoding: 'utf8' }).status
834
+ check('exit 0 on a valid finding', runV(fnd, okCtx), 0)
835
+ // A PR that deletes the rule must not make its own violation pass: base rules win.
836
+ const spoofCtx = join(crashDir, 'spoof.json')
837
+ wfPr(spoofCtx, JSON.stringify({ projectRules: 'nothing here', baseProjectRules: 'NEVER call console.log in src/.' }))
838
+ check('quote found in base rules is accepted even when the PR tree dropped it', runV(fnd, spoofCtx), 0)
839
+ const addedCtx = join(crashDir, 'added.json')
840
+ wfPr(addedCtx, JSON.stringify({ projectRules: 'NEVER call console.log in src/.', baseProjectRules: '' }))
841
+ check('a rule only the PR tree contains is rejected when base rules exist', runV(fnd, addedCtx), 1)
842
+ const weakenedFile = join(crashDir, 'weakened.json')
843
+ wfPr(weakenedFile, JSON.stringify(weakenedFinding))
844
+ check('validator accepts a weakened-rule quote found in baseProjectRules',
845
+ runV(weakenedFile, spoofCtx), 0)
846
+ check('validator rejects a weakened-rule quote found only in projectRules',
847
+ runV(weakenedFile, addedCtx), 1)
848
+ wfPr(join(crashDir, 'rej.json'), JSON.stringify({ rule: 'PROJECT:no-eval', ruleQuote: 'NEVER call eval anywhere in code' }))
849
+ check('exit 1 on a rejected finding', runV(join(crashDir, 'rej.json'), okCtx), 1)
850
+ const symlinkValidator = join(crashDir, 'project-rule-findings.mjs')
851
+ syPr(join(SKILL, 'scripts', 'project-rule-findings.mjs'), symlinkValidator)
852
+ check('exit 1 on a rejected finding through a symlink',
853
+ spawnPr('node', [symlinkValidator, join(crashDir, 'rej.json'), okCtx], { encoding: 'utf8' }).status, 1)
854
+ check('exit 2 on a missing context file', runV(fnd, join(crashDir, 'nope.json')), 2)
855
+ check('exit 2 on a malformed context file', runV(fnd, badJson), 2)
856
+ check('exit 2 on a malformed finding file', runV(badJson, okCtx), 2)
857
+ rmPr(crashDir, { recursive: true, force: true })
858
+
699
859
  console.log(failed === 0 ? '\nall checks passed\n' : `\n${failed} check(s) FAILED\n`)
700
860
  process.exit(failed === 0 ? 0 : 1)
@@ -134,6 +134,20 @@ collect_project_rules() {
134
134
  -not -path '*/node_modules/*' 2>/dev/null | sort)
135
135
  }
136
136
 
137
+ # Hard rules must come from a tree the PR author does not control: a PR that
138
+ # deletes a rule and then breaks it would otherwise leave nothing to enforce.
139
+ collect_project_rules_at_ref() {
140
+ local repo="$1" ref="$2" dir status=0
141
+ dir="$(mktemp -d "/tmp/lekker-base-rules-XXXXXX")"
142
+ if git -C "$repo" archive "$ref" 2>/dev/null | tar -x -C "$dir" 2>/dev/null; then
143
+ collect_project_rules "$dir"
144
+ else
145
+ status=1
146
+ fi
147
+ rm -rf "$dir"
148
+ return "$status"
149
+ }
150
+
137
151
  # Scan depth needs PR-authored review guidance but skips expensive worktree
138
152
  # setup. Read the PR tree from an archive so no Git worktree or checkout is made.
139
153
  if [[ "${LEKKER_RULES_ONLY:-}" == "1" ]]; then
@@ -149,14 +163,40 @@ if [[ "${LEKKER_RULES_ONLY:-}" == "1" ]]; then
149
163
  exit 1
150
164
  fi
151
165
  fi
166
+ if [[ -n "${LEKKER_BASE_REF:-}" ]]; then
167
+ if [[ -z "$LOCAL_REPO" && "$LEKKER_BASE_REF" == origin/* ]]; then
168
+ BASE_BRANCH="${LEKKER_BASE_REF#origin/}"
169
+ if ! git -C "$REPO_ROOT" fetch origin \
170
+ "+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}" 2>/dev/null; then
171
+ printf 'ERROR: could not fetch base ref %s for rules-only scan\n' "$LEKKER_BASE_REF" >&2
172
+ exit 1
173
+ fi
174
+ fi
175
+ if ! git -C "$REPO_ROOT" rev-parse --verify -q "${LEKKER_BASE_REF}^{commit}" >/dev/null; then
176
+ printf 'ERROR: base ref %s does not resolve for rules-only scan\n' "$LEKKER_BASE_REF" >&2
177
+ exit 1
178
+ fi
179
+ fi
152
180
  RULES_SOURCE="/tmp/lekker-rules-source-$$"
153
181
  mkdir -p "$RULES_SOURCE"
154
182
  git -C "$REPO_ROOT" archive "$RULES_REF" | tar -x -C "$RULES_SOURCE"
155
183
  PROJECT_RULES="$(collect_project_rules "$RULES_SOURCE")"
156
- python3 - "$OUT_JSON" "$PROJECT_RULES" <<'PYEOF'
184
+ BASE_PROJECT_RULES=""
185
+ HAVE_BASE_RULES=0
186
+ if [[ -n "${LEKKER_BASE_REF:-}" ]]; then
187
+ if BASE_PROJECT_RULES="$(collect_project_rules_at_ref "$REPO_ROOT" "$LEKKER_BASE_REF")"; then
188
+ HAVE_BASE_RULES=1
189
+ else
190
+ BASE_PROJECT_RULES=""
191
+ NOTES="${NOTES}base-rules-unavailable;"
192
+ fi
193
+ fi
194
+ python3 - "$OUT_JSON" "$PROJECT_RULES" "$BASE_PROJECT_RULES" "$HAVE_BASE_RULES" <<'PYEOF'
157
195
  import json, sys
196
+ out = {"projectRules": sys.argv[2]}
197
+ out["baseProjectRules"] = sys.argv[3] if sys.argv[4] == "1" else None
158
198
  with open(sys.argv[1], "w") as f:
159
- json.dump({"projectRules": sys.argv[2]}, f, indent=2)
199
+ json.dump(out, f, indent=2)
160
200
  PYEOF
161
201
  rm -rf "$RULES_SOURCE"
162
202
  printf 'OK %s\n' "$OUT_JSON"
@@ -475,6 +515,20 @@ PROJECT_RULES_FILE="/tmp/lekker-rules-$$.txt"
475
515
  printf '' > "$PROJECT_RULES_FILE"
476
516
  collect_project_rules "$WORKTREE_PATH" > "$PROJECT_RULES_FILE"
477
517
 
518
+ BASE_RULES_FILE="/tmp/lekker-base-rules-$$.txt"
519
+ BASE_RULES_STATE="absent"
520
+ printf '' > "$BASE_RULES_FILE"
521
+ if [[ -n "$MERGE_BASE_NOTE" && "$MERGE_BASE_NOTE" != "unknown" ]] \
522
+ && git -C "$REPO_ROOT" rev-parse --verify -q "${MERGE_BASE_NOTE}^{commit}" >/dev/null; then
523
+ if collect_project_rules_at_ref "$REPO_ROOT" "$MERGE_BASE_NOTE" > "$BASE_RULES_FILE"; then
524
+ BASE_RULES_STATE="present"
525
+ else
526
+ NOTES="${NOTES}base-rules-unavailable;"
527
+ fi
528
+ else
529
+ NOTES="${NOTES}base-rules-unavailable;"
530
+ fi
531
+
478
532
  # ---------------------------------------------------------------------------
479
533
  # 6. HEAD SHA
480
534
  # ---------------------------------------------------------------------------
@@ -518,14 +572,17 @@ python3 - \
518
572
  "$DEPS_INSTALL_FILE" \
519
573
  "$ESLINT_JSON_FILE" \
520
574
  "$TEST_RUNNER_JSON" \
521
- "$OUT_JSON" <<'PYEOF'
575
+ "$OUT_JSON" \
576
+ "$BASE_RULES_FILE" \
577
+ "$BASE_RULES_STATE" <<'PYEOF'
522
578
  import json, sys, os
523
579
 
524
580
  (worktree_path, repo_root, head_sha, head_sha_short,
525
581
  tsc_file, eslint_file, rules_file, delta_file, notes,
526
582
  changed_files_file, base_ref_val, eslint_scope_file,
527
583
  tsc_changed_file, tsc_count_file, merge_base_val,
528
- deps_install_file, eslint_json_file, test_runner_json, out_json) = sys.argv[1:20]
584
+ deps_install_file, eslint_json_file, test_runner_json, out_json,
585
+ base_rules_file, base_rules_state) = sys.argv[1:22]
529
586
 
530
587
  def parse_runner(raw):
531
588
  try:
@@ -581,6 +638,7 @@ data = {
581
638
  "tscTail": read_file(tsc_file),
582
639
  "eslintTail": read_file(eslint_file),
583
640
  "projectRules": read_file(rules_file),
641
+ "baseProjectRules": read_file(base_rules_file) if base_rules_state == "present" else None,
584
642
  "deltaFile": None if delta_file == "null" else delta_file,
585
643
  "notes": notes,
586
644
  "changedFiles": changed_files,
@@ -598,7 +656,7 @@ data = {
598
656
  "eslintJson": eslint_json_file if file_has_content(eslint_json_file) else None,
599
657
  }
600
658
 
601
- for p in (tsc_file, eslint_file, rules_file, changed_files_file,
659
+ for p in (tsc_file, eslint_file, rules_file, base_rules_file, changed_files_file,
602
660
  eslint_scope_file, tsc_changed_file, tsc_count_file,
603
661
  deps_install_file):
604
662
  try:
@@ -33,6 +33,7 @@ const FINDINGS_ARRAY_SCHEMA = {
33
33
  anyOf: [
34
34
  { properties: { severity: { enum: ['observation', 'idiomatic'] } } },
35
35
  { required: ['boundary'] },
36
+ { required: ['rule'], properties: { rule: { pattern: '^(PROJECT:[a-z0-9]+(-[a-z0-9]+)*|weakens-project-rule)$' } } },
36
37
  ],
37
38
  properties: {
38
39
  file: { type: 'string' },
@@ -44,6 +45,8 @@ const FINDINGS_ARRAY_SCHEMA = {
44
45
  fix: { type: 'string' },
45
46
  precedent: { type: 'string' },
46
47
  rule: { type: 'string', minLength: 1, pattern: '\\S' },
48
+ // Verbatim project rule a PROJECT:<slug> finding enforces.
49
+ ruleQuote: { type: 'string' },
47
50
  boundary: BOUNDARY_SCHEMA,
48
51
  // Short kebab-case slug naming the class of bug ("missing-null-check").
49
52
  bugClass: { type: 'string', minLength: 1, pattern: '^[a-z0-9]+(-[a-z0-9]+)*$' },
@@ -107,6 +110,8 @@ const VERDICT_SCHEMA = {
107
110
  verdict: { enum: ['confirmed', 'downgraded', 'dropped'] },
108
111
  newSeverity: { enum: ['important', 'observation'] },
109
112
  reasoning: { type: 'string' },
113
+ // True when the PROJECT-rule validator could not run (exit other than 0 or 1).
114
+ validatorUnavailable: { type: 'boolean' },
110
115
  },
111
116
  }
112
117
 
@@ -162,6 +167,23 @@ function titleTokens(title) {
162
167
  }
163
168
 
164
169
  function sameIssue(a, b) {
170
+ // Lens-generated slugs can differ even when they identify the same project rule.
171
+ const sameProjectQuote = typeof a.rule === 'string' && a.rule.startsWith('PROJECT:')
172
+ && typeof b.rule === 'string' && b.rule.startsWith('PROJECT:')
173
+ && typeof a.ruleQuote === 'string' && a.ruleQuote.trim() !== ''
174
+ && a.ruleQuote === b.ruleQuote
175
+ if (typeof a.rule === 'string' && typeof b.rule === 'string' && a.rule !== b.rule
176
+ && (a.rule.startsWith('PROJECT:') || b.rule.startsWith('PROJECT:'))
177
+ && !sameProjectQuote) {
178
+ return false
179
+ }
180
+ // Two quotes of one PROJECT rule that differ are two different rule passages, not one issue.
181
+ if (typeof a.rule === 'string' && a.rule === b.rule && a.rule.startsWith('PROJECT:')
182
+ && typeof a.ruleQuote === 'string' && a.ruleQuote.trim() !== ''
183
+ && typeof b.ruleQuote === 'string' && b.ruleQuote.trim() !== ''
184
+ && a.ruleQuote !== b.ruleQuote) {
185
+ return false
186
+ }
165
187
  // Merging two classes would send one class's instances to the other's fix.
166
188
  if (a.bugClass && b.bugClass && a.bugClass !== b.bugClass) {
167
189
  return false
@@ -182,6 +204,22 @@ const BUG_CLASS_INSTRUCTION = `BUG CLASS: give every finding a "bugClass" (short
182
204
  + ` hit, search the whole diff for the same pattern and list every other place as`
183
205
  + ` "file:line" (empty array when there is none). Report the class, not just the first hit.`
184
206
 
207
+ const PROJECT_RULE_DIMENSIONS = ['quality', 'implementation', 'simplification', 'conventions', 'test-quality',
208
+ 'triage-quality', 'triage-logic']
209
+
210
+ const PROJECT_RULE_INSTRUCTION = `PROJECT RULES AS HARD RULES: read "projectRules" from CONTEXT_FILE.`
211
+ + ` When a changed line violates a project rule written as a prohibition or an obligation`
212
+ + ` (NEVER, MUST, MUST NOT, ALWAYS, DO NOT, or a plain-English equivalent), raise a Critical`
213
+ + ` finding with "rule" set to "PROJECT:<short-kebab-slug>", "ruleQuote" set to the exact`
214
+ + ` sentence from the rules (use "baseProjectRules" when it is a string: it is the base`
215
+ + ` branch's copy, which the PR author cannot edit; otherwise "projectRules"), and file:line`
216
+ + ` on the violating line. When the diff deletes or weakens a rule line that is in`
217
+ + ` baseProjectRules, raise an Important finding with rule "weakens-project-rule" quoting`
218
+ + ` the removed line. One finding per rule,`
219
+ + ` with every other violating site in "otherInstances". Never quote a rule that is not in`
220
+ + ` the authoritative source ("baseProjectRules" when it is a string, otherwise "projectRules"). Preferences and suggestions are not hard rules: report them as ordinary`
221
+ + ` findings. A REVIEW.md ignore entry narrows where you look, it never disables a rule.`
222
+
185
223
  const BUG_CLASS_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/
186
224
  const INSTANCE_PATTERN = new RegExp(INSTANCE_PATTERN_SOURCE)
187
225
 
@@ -234,6 +272,14 @@ function mergeFindings(a, b) {
234
272
 
235
273
  if (!merged.rule && b.rule) {
236
274
  merged.rule = b.rule
275
+ if (Object.prototype.hasOwnProperty.call(b, 'ruleQuote')) {
276
+ merged.ruleQuote = b.ruleQuote
277
+ } else {
278
+ delete merged.ruleQuote
279
+ }
280
+ } else if (merged.rule && merged.rule === b.rule && !merged.ruleQuote
281
+ && typeof b.ruleQuote === 'string' && b.ruleQuote.trim()) {
282
+ merged.ruleQuote = b.ruleQuote
237
283
  }
238
284
 
239
285
  const agreedBy = []
@@ -547,7 +593,7 @@ if (Number(cycle) >= RETHINK_CYCLE) {
547
593
  recurringClasses: Array.isArray(recurringClasses) ? recurringClasses : [],
548
594
  findings: [],
549
595
  preExisting: [],
550
- droppedCount: 0, downgradedCount: 0, hardRuleCount: 0, agentCount: 0,
596
+ droppedCount: 0, downgradedCount: 0, hardRuleCount: 0, validatorUnavailableCount: 0, agentCount: 0,
551
597
  proveAttemptCount: 0, provenCount: 0, suppressedCount: 0,
552
598
  outputTokens: 0, turnTokensTotal: budget.spent(),
553
599
  engine: ENGINE,
@@ -562,6 +608,7 @@ const budgetAtStart = budget.spent()
562
608
  let droppedCount = 0
563
609
  let downgradedCount = 0
564
610
  let hardRuleCount = 0
611
+ let validatorUnavailableCount = 0
565
612
  let provenCount = 0
566
613
  let suppressedCount = 0
567
614
  let proveAttemptCount = 0
@@ -613,6 +660,10 @@ const budgetAtStart = budget.spent()
613
660
 
614
661
  prompt += ' ' + BUG_CLASS_INSTRUCTION
615
662
 
663
+ if (PROJECT_RULE_DIMENSIONS.includes(dim.key)) {
664
+ prompt += ' ' + PROJECT_RULE_INSTRUCTION
665
+ }
666
+
616
667
  // The deterministic lint pre-flight has already collected every rule the
617
668
  // repo's own eslint config fires on this diff, and those are exactly the
618
669
  // comments the bot reviewers bill for. An agent re-reporting them costs a
@@ -650,7 +701,7 @@ const budgetAtStart = budget.spent()
650
701
  `FINDING (JSON): ${JSON.stringify(finding)}.`,
651
702
  `DIFF_FILE=${diffFile}, CONTEXT_FILE=${contextFile}, WORKTREE_PATH=${wtDisplay}.`,
652
703
  `HOUSE_RULES_FILE: read key "houseRulesFile" from CONTEXT_FILE, then read that exact path before validating a hard rule.`,
653
- `Default to dropping when uncertain.`,
704
+ `Default to dropping when uncertain, except when the PROJECT-rule validator was unavailable: then check by hand that the exact quote appears in baseProjectRules when it is a string, otherwise projectRules (drop if it does not), keep the finding only if it does, set validatorUnavailable true, and judge applicability.`,
654
705
  ].join(' ')
655
706
  }
656
707
 
@@ -708,6 +759,9 @@ const budgetAtStart = budget.spent()
708
759
  // A model verdict cannot replace the required production boundary map.
709
760
  function enforceBoundary(finding) {
710
761
  if (!shouldVerify(finding, depth)) return finding
762
+ // A project rule or a weakened project rule is violated by the line itself; it needs no production entry point.
763
+ if (typeof finding.rule === 'string'
764
+ && (finding.rule.startsWith('PROJECT:') || finding.rule === 'weakens-project-rule')) return finding
711
765
  const boundary = finding.boundary
712
766
  const hasText = function(value) {
713
767
  return typeof value === 'string' && value.trim().length > 0
@@ -764,6 +818,11 @@ const budgetAtStart = budget.spent()
764
818
  return kept
765
819
  }
766
820
 
821
+ if (verdict.validatorUnavailable === true) {
822
+ validatorUnavailableCount++
823
+ log(`PROJECT-rule validator unavailable for ${finding.file}:${finding.line}; ${verdict.verdict === 'dropped' ? 'finding dropped' : 'finding kept'}`)
824
+ }
825
+
767
826
  if (verdict.verdict === 'dropped') {
768
827
  droppedCount++
769
828
  return null
@@ -1087,6 +1146,7 @@ const budgetAtStart = budget.spent()
1087
1146
  droppedCount,
1088
1147
  downgradedCount,
1089
1148
  hardRuleCount,
1149
+ validatorUnavailableCount,
1090
1150
  agentCount,
1091
1151
  proveAttemptCount,
1092
1152
  provenCount,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-obsidian",
3
3
  "description": "Keep an Obsidian vault in sync with work: PR sync, task rollover, morning routine.",
4
- "version": "1.69.4",
4
+ "version": "1.71.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-product",
3
3
  "description": "Take a product idea to a shippable build: MVP passes, full-stack scaffolds, UX/UI quality loops, launch plans.",
4
- "version": "1.69.4",
4
+ "version": "1.71.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-reflection",
3
3
  "description": "Look back and improve: self-critique, retrospectives, performance review, rapid learning.",
4
- "version": "1.69.4",
4
+ "version": "1.71.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: team-retrospective
3
+ description: Collect a factual retrospective from each task-owned agent and safely dismiss the team after successful work. Use during wrap-up of a completed, green multi-agent task or when explicitly requested.
4
+ license: MIT
5
+ compatibility: Codex, Claude Code, and other agent environments with delegation controls.
6
+ metadata:
7
+ author: Oleg Koval
8
+ tags:
9
+ - agents
10
+ - retrospective
11
+ - delegation
12
+ - cleanup
13
+ ---
14
+
15
+ # Team retrospective
16
+
17
+ Run after a multi-agent task is complete and its applicable checks are green,
18
+ before dismissing its agents. Explicit user instructions to retain agents or
19
+ finish early take precedence. Do not spawn agents solely to run this skill.
20
+
21
+ 1. Identify agents, tabs, and sessions owned by this task using the environment's
22
+ actual inventory and recorded ownership. Preserve other tasks and sessions.
23
+ 2. Ask each reachable agent for a concise factual report: what went wrong, failed
24
+ approaches, tool or coordination problems, resolutions, and unresolved items.
25
+ Ask them to distinguish direct observation from inference. A clean report may
26
+ say no issues. Do not request new implementation or speculative findings.
27
+ 3. Collect each response, or label it unavailable with the reason. Preserve useful
28
+ findings and a concise summary in an appropriate task artifact; avoid secrets,
29
+ unnecessary transcripts, and public disclosure. Report recurring issues and
30
+ unresolved items to the user. Claims remain reports until independently verified.
31
+ 4. Confirm work and required handoffs are preserved. Dismiss the task-owned agents
32
+ and close only their task-owned tabs or panes when safe; verify the result.
33
+ Follow the environment's skill and safety rules. Never stop a shared server or
34
+ close another task's session. Respect explicit instructions to keep agents.
35
+ 5. If work is blocked, unfinished, or still active, preserve the agents and report
36
+ the blocker instead of treating the task as green. If the environment cannot
37
+ contact or dismiss an agent, report the limitation; never claim it was dismissed.
@@ -42,6 +42,8 @@ Use this as a proportional end-of-task audit. Preserve pre-existing work and do
42
42
  - Check the diff and untracked files for keys, tokens, passwords, private keys, connection strings, cookies, personal data, and credentials in logs or examples. Report locations without printing values; recommend environment variables or a secret manager where relevant.
43
43
  - Decide from the actual change whether README, architecture or API docs, OpenAPI, CHANGELOG, CONTRIBUTING, `.env.example`, ADRs, or runbooks need an update. Name the exact change needed. Skip documentation for trivial internal changes unless it helps future maintenance.
44
44
 
45
+ Before cleanup, when a completed task used a team and its applicable checks are green, invoke `olko:team-retrospective` to collect each agent’s report and safely dismiss task-owned agents. If unavailable, report the limitation; do not claim closeout ran.
46
+
45
47
  ## 4. Prepare safe cleanup
46
48
 
47
49
  - Propose cleanup commands only. Do not remove any file, branch, or worktree automatically, including generated or temporary files. Never propose deletion of an unknown untracked file, broad directory, or unresolved glob.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-release",
3
3
  "description": "Ship a release: semantic-release setup, changelogs, store listing copy, release-day routine.",
4
- "version": "1.69.4",
4
+ "version": "1.71.0",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },