@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.
- package/.github/copilot-instructions.md +1 -0
- package/.github/prompts/team-retrospective.prompt.md +31 -0
- package/.github/prompts/wrap-up.prompt.md +2 -0
- package/.kiro/steering/team-retrospective.md +30 -0
- package/.kiro/steering/wrap-up.md +2 -0
- package/.windsurf/rules/team-retrospective.md +29 -0
- package/.windsurf/rules/wrap-up.md +2 -0
- package/README.md +1 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +11 -2
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/conventions.md +3 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/implementation.md +3 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/quality.md +3 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/simplification.md +3 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/test-quality.md +3 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/verifier.md +28 -3
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/project-rules/CLAUDE.md +4 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/fixtures/project-rules/planted.diff +9 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/project-rule-findings.mjs +122 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +160 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +63 -5
- package/adapters/claude/olko-reflection/skills/team-retrospective/SKILL.md +38 -0
- package/adapters/claude/olko-reflection/skills/wrap-up/SKILL.md +2 -0
- package/adapters/codex/olko-reflection/README.md +1 -0
- package/adapters/cursor/olko-reflection/skills/team-retrospective/SKILL.md +38 -0
- package/adapters/cursor/olko-reflection/skills/wrap-up/SKILL.md +2 -0
- package/adapters/grok/olko-reflection/skills/team-retrospective/SKILL.md +38 -0
- package/adapters/grok/olko-reflection/skills/wrap-up/SKILL.md +2 -0
- package/adapters/hermes/olko-reflection/README.md +1 -0
- package/catalog/skills.json +38 -2
- package/package.json +1 -1
- package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +11 -2
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/conventions.md +3 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/implementation.md +3 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/quality.md +3 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/simplification.md +3 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/test-quality.md +3 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/verifier.md +28 -3
- package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/project-rules/CLAUDE.md +4 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/fixtures/project-rules/planted.diff +9 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/project-rule-findings.mjs +122 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +160 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +63 -5
- package/plugins/olko-github-pr/skills/lekker-review/workflow.js +62 -2
- package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-reflection/skills/team-retrospective/SKILL.md +37 -0
- package/plugins/olko-reflection/skills/wrap-up/SKILL.md +2 -0
- package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
- 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
|
-
|
|
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(
|
|
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"
|
|
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
|
|
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,
|
|
@@ -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.
|