Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9598298
fix: Prevent task decomposer from splitting compound words on unspace…
stranske Jan 12, 2026
2f4ab62
chore(autofix): formatting/lint
github-actions[bot] Jan 12, 2026
00e5b75
fix: Strip code blocks before parsing tasks in issue_scope_parser
stranske Jan 12, 2026
a62c1cc
fix: preserve code blocks in PR status summary without adding checkboxes
stranske Jan 12, 2026
dfd66e8
fix: apply same slash fix to _is_multi_action_task in capability_chec…
stranske Jan 12, 2026
96ef587
feat: add template sync validation to prevent consumer repo sync fail…
stranske Jan 12, 2026
79a9c94
chore(autofix): formatting/lint
github-actions[bot] Jan 12, 2026
53406d9
docs: add template sync guidance to SYNC_WORKFLOW and SETUP_CHECKLIST
stranske Jan 12, 2026
f05d1b6
fix: address Codex review - fail on missing templates, sync creates n…
stranske Jan 12, 2026
8a5df02
fix: add health-72-template-sync to workflow inventory and tests
stranske Jan 12, 2026
fbfe3c5
Merge branch 'main' into fix/template-sync-guard
stranske Jan 12, 2026
a76f6f1
test: Add comprehensive coverage for validate_template_sync.py
stranske Jan 12, 2026
16885b7
chore(autofix): formatting/lint
github-actions[bot] Jan 12, 2026
364f1f0
sync: Create 13 missing template files (FIX: actually commit them thi…
stranske Jan 12, 2026
761f41d
fix: Actually rename workflow file to health-72-template-sync.yml
stranske Jan 12, 2026
04f11b4
fix: Update workflow display name to match filename
stranske Jan 12, 2026
1a865ca
fix: Make test_validate_template_sync tests work properly
stranske Jan 12, 2026
de7d273
chore(autofix): formatting/lint
github-actions[bot] Jan 12, 2026
7725d8b
fix: Update test expectation for missing template directory
stranske Jan 12, 2026
77b9331
chore: retrigger CI checks
stranske Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/scripts/__tests__/issue_scope_parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,69 @@ test('handles nested bullets', () => {
].join('\n')
);
});

test('preserves code blocks but does not add checkboxes inside them', () => {
const issue = [
'## Tasks',
'- Implement the feature',
'- Add documentation',
'',
'```python',
'def example():',
' # Code example with bullet that should NOT get checkbox',
' tasks = ["- item one", "- item two"]',
'```',
'',
'## Acceptance Criteria',
'- Feature works correctly',
].join('\n');

const result = extractScopeTasksAcceptanceSections(issue);

// Real tasks should get checkboxes
assert.ok(result.includes('- [ ] Implement the feature'));
assert.ok(result.includes('- [ ] Add documentation'));
assert.ok(result.includes('- [ ] Feature works correctly'));

// Code block should be preserved but items inside should NOT have checkboxes
assert.ok(result.includes('```python'));
assert.ok(result.includes('def example():'));
// The bullet inside the code should NOT have [ ] added
assert.ok(!result.includes('- [ ] item one'));
});

test('handles multiple code blocks - preserves content without adding checkboxes', () => {
const issue = [
'## Tasks',
'- Real task one',
'',
'```yaml',
'tasks:',
' - YAML example task',
'```',
'',
'- Real task two',
'',
'~~~markdown',
'- Markdown example in tilde fence',
'~~~',
'',
'## Acceptance Criteria',
'- Done',
].join('\n');

const result = extractScopeTasksAcceptanceSections(issue);

// Real tasks outside code blocks get checkboxes
assert.ok(result.includes('- [ ] Real task one'));
assert.ok(result.includes('- [ ] Real task two'));
assert.ok(result.includes('- [ ] Done'));

// Code blocks are preserved
assert.ok(result.includes('```yaml'));
assert.ok(result.includes('~~~markdown'));

// But items inside code blocks do NOT get checkboxes added
assert.ok(result.includes(' - YAML example task')); // preserved as-is
assert.ok(!result.includes('- [ ] YAML example task')); // no checkbox added
});
101 changes: 86 additions & 15 deletions .github/scripts/issue_scope_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
const normalizeNewlines = (value) => String(value || '').replace(/\r\n/g, '\n');
const stripBlockquotePrefixes = (value) =>
String(value || '').replace(/^[ \t]*>+[ \t]?/gm, '');

/**
* Check if a line is a code fence delimiter (``` or ~~~).
* Used to track code block boundaries when processing content.
*/
const isCodeFenceLine = (line) => /^(`{3,}|~{3,})/.test(line.trim());

const LIST_ITEM_REGEX = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/;

const SECTION_DEFS = [
Expand Down Expand Up @@ -84,7 +91,20 @@ function normaliseChecklist(content) {

const lines = raw.split('\n');
let mutated = false;
let insideCodeBlock = false;

const updated = lines.map((line) => {
// Track code fence boundaries - don't add checkboxes inside code blocks
if (isCodeFenceLine(line)) {
insideCodeBlock = !insideCodeBlock;
return line;
}

// Skip checkbox normalization for lines inside code blocks
if (insideCodeBlock) {
return line;
}

const match = line.match(LIST_ITEM_REGEX);
if (!match) {
return line;
Expand Down Expand Up @@ -155,6 +175,7 @@ function isExplicitHeadingLine(rawLine) {
function extractListBlocks(lines) {
const blocks = [];
let current = [];
let insideCodeBlock = false;

const flush = () => {
if (current.length) {
Expand All @@ -167,6 +188,24 @@ function extractListBlocks(lines) {
};

for (const line of lines) {
// Track code fence boundaries
if (isCodeFenceLine(line)) {
insideCodeBlock = !insideCodeBlock;
// Include code fence lines in current block if we're building one
if (current.length) {
current.push(line);
}
continue;
}

// Lines inside code blocks are treated as continuation (don't break the block)
if (insideCodeBlock) {
if (current.length) {
current.push(line);
}
continue;
}

if (LIST_ITEM_REGEX.test(line)) {
current.push(line);
continue;
Expand All @@ -190,6 +229,7 @@ function extractListBlocksWithOffsets(lines) {
let blockStart = null;
let blockEnd = null;
let offset = 0;
let insideCodeBlock = false;

const flush = () => {
if (!current.length) {
Expand All @@ -205,6 +245,27 @@ function extractListBlocksWithOffsets(lines) {
};

for (const line of lines) {
// Track code fence boundaries
if (isCodeFenceLine(line)) {
insideCodeBlock = !insideCodeBlock;
if (current.length) {
current.push(line);
blockEnd = offset + line.length;
}
offset += line.length + 1;
continue;
}

// Lines inside code blocks continue the current block
if (insideCodeBlock) {
if (current.length) {
current.push(line);
blockEnd = offset + line.length;
}
offset += line.length + 1;
continue;
}

const isList = LIST_ITEM_REGEX.test(line);
if (isList) {
if (!current.length) {
Expand Down Expand Up @@ -377,23 +438,33 @@ function collectSections(source) {
const lines = segment.split('\n');
const listBlocks = extractListBlocksWithOffsets(lines);
let offset = 0;
let insideCodeBlock = false;
for (const line of lines) {
const matchedLabel = extractHeadingLabel(line);
if (matchedLabel) {
const title = matchedLabel.toLowerCase();
if (aliasLookup[title]) {
const section = aliasLookup[title];
headings.push({
title: section.key,
label: section.label,
index: offset,
length: line.length,
matchedLabel,
});
}
// Track code fence boundaries - skip heading detection inside code blocks
if (isCodeFenceLine(line)) {
insideCodeBlock = !insideCodeBlock;
offset += line.length + 1;
continue;
}
if (isExplicitHeadingLine(line)) {
allHeadings.push({ index: offset, length: line.length });

if (!insideCodeBlock) {
const matchedLabel = extractHeadingLabel(line);
if (matchedLabel) {
const title = matchedLabel.toLowerCase();
if (aliasLookup[title]) {
const section = aliasLookup[title];
headings.push({
title: section.key,
label: section.label,
index: offset,
length: line.length,
matchedLabel,
});
}
}
if (isExplicitHeadingLine(line)) {
allHeadings.push({ index: offset, length: line.length });
}
}
offset += line.length + 1;
}
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/validate-template-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Validate Template Sync

on:
pull_request:
paths:
- '.github/scripts/**/*.js'
- 'templates/consumer-repo/.github/scripts/**/*.js'
push:
branches: [main]
paths:
- '.github/scripts/**/*.js'
- 'templates/consumer-repo/.github/scripts/**/*.js'

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Validate template sync
run: |
python scripts/validate_template_sync.py
if [ $? -ne 0 ]; then
echo ""
echo "❌ FAILED: Template files are out of sync!"
echo ""
echo "To fix this error:"
echo " 1. Run: ./scripts/sync_templates.sh"
echo " 2. Commit the changes to templates/consumer-repo/"
echo " 3. Push to your branch"
echo ""
exit 1
fi
Comment thread
stranske marked this conversation as resolved.
Outdated
19 changes: 19 additions & 0 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,27 @@ Before submitting changes, run the validation scripts:

# Comprehensive check (30-120 seconds)
./scripts/check_branch.sh

# Template sync validation (if you modified .github/scripts/)
python scripts/validate_template_sync.py
```

### ⚠️ CRITICAL: Template Sync Guard

**If you modify any file in `.github/scripts/`, you MUST also update the template:**

```bash
# After editing .github/scripts/*.js, run:
./scripts/sync_templates.sh

# Verify sync:
python scripts/validate_template_sync.py
```

**Why?** Consumer repos get workflow updates via `templates/consumer-repo/`. If you only update `.github/scripts/` but not the template, your changes won't sync to consumer repos and no sync PRs will be created.

The CI will fail if templates are out of sync with source files.

## Code Style

### Python
Expand Down
2 changes: 1 addition & 1 deletion scripts/langchain/capability_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def _is_multi_action_task(task: str) -> bool:
return True
if any(sep in lowered for sep in (" and ", " + ", " & ", " then ", "; ")):
return True
return bool("," in task or "/" in task or re.search(r"\s\+\s", lowered))
return bool("," in task or " / " in task or re.search(r"\s\+\s", lowered))


def _requires_admin_access(task: str) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion scripts/langchain/issue_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ def _is_large_task(task: str) -> bool:
return True
if any(sep in lowered for sep in (" and ", " + ", " & ", " then ", "; ")):
return True
return bool(re.search(r"\s\+\s", lowered) or ", " in task or "/" in task)
return bool(re.search(r"\s\+\s", lowered) or ", " in task or " / " in task)


def _detect_task_splitting(tasks: list[str], *, use_llm: bool = False) -> list[dict[str, Any]]:
Expand Down
6 changes: 4 additions & 2 deletions scripts/langchain/task_decomposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,10 @@ def _split_task_parts(task: str) -> list[str]:
parts = [part.strip() for part in task.split(";") if part.strip()]
elif ", " in task:
parts = [part.strip() for part in task.split(",") if part.strip()]
elif " / " in task or "/" in task:
parts = [part.strip() for part in re.split(r"\s*/\s*", task) if part.strip()]
elif " / " in task:
# Only split on spaced slashes to avoid splitting compound words
# like "additions/removals" or paths like "src/utils"
parts = [part.strip() for part in task.split(" / ") if part.strip()]
else:
parts = [task]
return [part for part in parts if part]
Expand Down
34 changes: 34 additions & 0 deletions scripts/sync_templates.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
# Sync source scripts to template directory
set -e

SOURCE_DIR=".github/scripts"
TEMPLATE_DIR="templates/consumer-repo/.github/scripts"

echo "🔄 Syncing scripts to template directory..."

# Get list of files to sync (exclude tests)
FILES=$(find "$SOURCE_DIR" -name "*.js" -type f \
| grep -v "__tests__" \
| grep -v ".test.js" \
| sed "s|^$SOURCE_DIR/||")

synced=0
for file in $FILES; do
source_file="$SOURCE_DIR/$file"
template_file="$TEMPLATE_DIR/$file"

if [ -f "$template_file" ]; then
if ! cmp -s "$source_file" "$template_file"; then
echo " ✓ Syncing $file"
cp "$source_file" "$template_file"
((synced++))
Comment thread
stranske marked this conversation as resolved.
Outdated
fi
Comment thread
stranske marked this conversation as resolved.
Outdated
fi
Comment thread
stranske marked this conversation as resolved.
Outdated
done

if [ $synced -eq 0 ]; then
echo "✅ All files already in sync"
else
echo "✅ Synced $synced file(s)"
fi
Loading
Loading