fix: clean promoted instinct sources (#2587)

* fix: clean promoted instinct sources

* test(instincts): normalize retained-content line endings

---------

Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
This commit is contained in:
Affaan Mustafa 2026-07-28 18:31:12 -07:00 committed by GitHub
parent 4e973d3eaf
commit 6be87a56ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 256 additions and 0 deletions

View file

@ -27,6 +27,7 @@ import ipaddress
import socket import socket
import urllib.parse import urllib.parse
import urllib.request import urllib.request
import tempfile
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@ -1323,6 +1324,106 @@ def _show_promotion_candidates(project: dict) -> None:
print(f" Run `instinct-cli.py promote` to promote these to global scope.\n") print(f" Run `instinct-cli.py promote` to promote these to global scope.\n")
def _frontmatter_scalar(lines: list[str], key: str) -> Optional[str]:
"""Extract a simple scalar value from frontmatter lines."""
for line in lines:
if ':' not in line:
continue
parsed_key, value = line.split(':', 1)
if parsed_key.strip() != key:
continue
value = value.strip()
if value.startswith('"') and value.endswith('"'):
return value[1:-1].replace('\\"', '"').replace('\\\\', '\\')
if value.startswith("'") and value.endswith("'"):
return value[1:-1].replace("''", "'")
return value
return None
def _remove_instinct_blocks(content: str, instinct_id: str) -> tuple[str, int]:
"""Remove raw frontmatter blocks with a matching instinct ID."""
lines = content.splitlines(keepends=True)
retained = []
removed = 0
index = 0
while index < len(lines):
if lines[index].strip() != '---':
retained.append(lines[index])
index += 1
continue
block_start = index
frontmatter_end = index + 1
while frontmatter_end < len(lines) and lines[frontmatter_end].strip() != '---':
frontmatter_end += 1
if frontmatter_end >= len(lines):
retained.extend(lines[block_start:])
break
next_block_start = frontmatter_end + 1
while next_block_start < len(lines) and lines[next_block_start].strip() != '---':
next_block_start += 1
block_id = _frontmatter_scalar(lines[block_start + 1:frontmatter_end], 'id')
if block_id == instinct_id:
removed += 1
else:
retained.extend(lines[block_start:next_block_start])
index = next_block_start
return ''.join(retained), removed
def _write_text_atomic(file_path: Path, content: str) -> None:
"""Replace a text file via same-directory temp file."""
temp_fd, temp_name = tempfile.mkstemp(
prefix=f".{file_path.name}.",
suffix=".tmp",
dir=file_path.parent,
text=True,
)
temp_file = Path(temp_name)
try:
with os.fdopen(temp_fd, "w", encoding="utf-8") as f:
f.write(content)
f.flush()
os.fsync(f.fileno())
os.replace(temp_file, file_path)
finally:
try:
temp_file.unlink()
except FileNotFoundError:
pass
def _remove_instinct_from_source(source_file_str: str, instinct_id: str) -> None:
"""Strip promoted instinct blocks from the project-scoped source file."""
source_file = Path(source_file_str)
if not source_file.exists():
return
try:
content = source_file.read_text(encoding="utf-8")
except OSError as exc:
print(f"Warning: Failed to read promoted instinct source {source_file}: {exc}", file=sys.stderr)
return
remaining_content, removed = _remove_instinct_blocks(content, instinct_id)
if removed == 0:
return
try:
if remaining_content:
_write_text_atomic(source_file, remaining_content)
else:
source_file.unlink()
except OSError as exc:
print(f"Warning: Failed to remove promoted instinct from {source_file}: {exc}", file=sys.stderr)
def cmd_promote(args) -> int: def cmd_promote(args) -> int:
"""Promote project-scoped instincts to global scope.""" """Promote project-scoped instincts to global scope."""
project = detect_project() project = detect_project()
@ -1385,6 +1486,9 @@ def _promote_specific(project: dict, instinct_id: str, force: bool, dry_run: boo
output_content += target.get('content', '') + "\n" output_content += target.get('content', '') + "\n"
output_file.write_text(output_content, encoding="utf-8") output_file.write_text(output_content, encoding="utf-8")
source_file = target.get('_source_file')
if source_file:
_remove_instinct_from_source(source_file, instinct_id)
print(f"\nPromoted '{instinct_id}' to global scope.") print(f"\nPromoted '{instinct_id}' to global scope.")
print(f" Saved to: {output_file}") print(f" Saved to: {output_file}")
return 0 return 0
@ -1458,6 +1562,10 @@ def _promote_auto(project: dict, force: bool, dry_run: bool) -> int:
output_content += inst.get('content', '') + "\n" output_content += inst.get('content', '') + "\n"
output_file.write_text(output_content, encoding="utf-8") output_file.write_text(output_content, encoding="utf-8")
for _, _, entry_inst in cand['entries']:
entry_source = entry_inst.get('_source_file')
if entry_source:
_remove_instinct_from_source(entry_source, cand['id'])
promoted += 1 promoted += 1
print(f"\nPromoted {promoted} instincts to global scope.") print(f"\nPromoted {promoted} instincts to global scope.")

View file

@ -63,6 +63,10 @@ function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8')); return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} }
function normalizeLineEndings(value) {
return value.replace(/\r\n/g, '\n');
}
function writeInstinct(filePath, id, confidence = 0.9) { function writeInstinct(filePath, id, confidence = 0.9) {
fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync( fs.writeFileSync(
@ -117,6 +121,13 @@ function runGit(cwd, args) {
return result.stdout.trim(); return result.stdout.trim();
} }
function initGitProject(parentDir, name = 'repo') {
const repoDir = path.join(parentDir, name);
fs.mkdirSync(repoDir, { recursive: true });
runGit(repoDir, ['init']);
return repoDir;
}
function runCli(root, args, options = {}) { function runCli(root, args, options = {}) {
return spawnSync(PYTHON3, [cliPath, ...args], { return spawnSync(PYTHON3, [cliPath, ...args], {
cwd: options.cwd || repoRoot, cwd: options.cwd || repoRoot,
@ -330,6 +341,143 @@ test('status migrates legacy no-remote linked worktree project dirs to main work
} }
}); });
test('promote removes only the promoted instinct block from project source', () => {
const root = createTempDir();
const repoParent = createTempDir();
try {
const repoDir = initGitProject(repoParent);
const projectId = projectHash(runGit(repoDir, ['rev-parse', '--show-toplevel']));
const sourceFile = path.join(root, 'projects', projectId, 'instincts', 'personal', 'mixed.yaml');
const retainedBlock = [
'---',
'id: keep-me',
'trigger: "when value: contains colon"',
'confidence: 0.72',
'domain: workflow',
'tags: [alpha, beta]',
'---',
'',
'Keep this block exactly.',
'',
].join('\n');
fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
fs.writeFileSync(
sourceFile,
[
'---',
'id: promote-me',
'trigger: "when promoting"',
'confidence: 0.91',
'domain: workflow',
'---',
'',
'Promote this block.',
'',
retainedBlock,
].join('\n')
);
const result = runCli(root, ['promote', 'promote-me', '--force'], { cwd: repoDir });
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(fs.existsSync(path.join(root, 'instincts', 'personal', 'promote-me.yaml')));
assert.strictEqual(normalizeLineEndings(fs.readFileSync(sourceFile, 'utf8')), retainedBlock);
} finally {
cleanupDir(root);
cleanupDir(repoParent);
}
});
test('promote deletes project source file when it only contained the promoted instinct', () => {
const root = createTempDir();
const repoParent = createTempDir();
try {
const repoDir = initGitProject(repoParent);
const projectId = projectHash(runGit(repoDir, ['rev-parse', '--show-toplevel']));
const sourceFile = path.join(root, 'projects', projectId, 'instincts', 'personal', 'single.yaml');
writeInstinct(sourceFile, 'promote-single', 0.93);
const result = runCli(root, ['promote', 'promote-single', '--force'], { cwd: repoDir });
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(fs.existsSync(path.join(root, 'instincts', 'personal', 'promote-single.yaml')));
assert.ok(!fs.existsSync(sourceFile));
} finally {
cleanupDir(root);
cleanupDir(repoParent);
}
});
test('promote preserves malformed and foreign source blocks while removing target', () => {
const root = createTempDir();
const repoParent = createTempDir();
try {
const repoDir = initGitProject(repoParent);
const projectId = projectHash(runGit(repoDir, ['rev-parse', '--show-toplevel']));
const sourceFile = path.join(root, 'projects', projectId, 'instincts', 'personal', 'foreign.yaml');
const foreignContent = [
'---',
'title: foreign block without id',
'---',
'',
'Do not drop this content.',
'',
'---',
'id: keep-foreign-neighbor',
'trigger: "when nearby"',
'confidence: not-a-float',
'---',
'',
'This parse-tolerated block must also stay raw.',
'',
].join('\n');
fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
fs.writeFileSync(
sourceFile,
[
foreignContent,
'---',
'id: promote-foreign',
'trigger: "when target appears"',
'confidence: 0.95',
'domain: workflow',
'---',
'',
'Only this block should be removed.',
'',
].join('\n')
);
const result = runCli(root, ['promote', 'promote-foreign', '--force'], { cwd: repoDir });
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(fs.existsSync(path.join(root, 'instincts', 'personal', 'promote-foreign.yaml')));
assert.strictEqual(normalizeLineEndings(fs.readFileSync(sourceFile, 'utf8')), `${foreignContent}\n`);
} finally {
cleanupDir(root);
cleanupDir(repoParent);
}
});
test('auto-promote removes promoted source copies from every contributing project', () => {
const root = createTempDir();
try {
const registryPath = path.join(root, 'projects.json');
const projectOne = seedProject(root, 'proj111', { personal: ['shared-auto'] });
const projectTwo = seedProject(root, 'proj222', { personal: ['shared-auto'] });
writeJson(registryPath, {
proj111: { name: 'one', root: '/repo/one', remote: '', last_seen: '2026-01-01T00:00:00Z' },
proj222: { name: 'two', root: '/repo/two', remote: '', last_seen: '2026-01-02T00:00:00Z' },
});
const result = runCli(root, ['promote', '--force'], { cwd: root });
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Promoted 1 instincts to global scope/);
assert.ok(fs.existsSync(path.join(root, 'instincts', 'personal', 'shared-auto.yaml')));
assert.ok(!fs.existsSync(path.join(projectOne, 'instincts', 'personal', 'shared-auto.yaml')));
assert.ok(!fs.existsSync(path.join(projectTwo, 'instincts', 'personal', 'shared-auto.yaml')));
} finally {
cleanupDir(root);
}
});
console.log(`\nPassed: ${passed}`); console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`); console.log(`Failed: ${failed}`);