-
-
Notifications
You must be signed in to change notification settings - Fork 35.6k
fix: instinct promote leaves orphaned project-scoped copy #2549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1314,6 +1314,45 @@ def _show_promotion_candidates(project: dict) -> None: | |
| print(f" Run `instinct-cli.py promote` to promote these to global scope.\n") | ||
|
|
||
|
|
||
| def _remove_instinct_from_source(source_file_str: str, instinct_id: str) -> None: | ||
| """Strip one instinct block from its project-scoped source file after promotion. | ||
|
|
||
| Without this, the project copy keeps shadowing the new global copy in | ||
| load_all_instincts()'s dedup (project wins on ID collision), so `status` | ||
| silently under-reports the global count until the stale copy is removed by hand. | ||
| Deletes the file if it was the only instinct in it, otherwise rewrites the | ||
| remaining blocks in the same frontmatter format. | ||
| """ | ||
| source_file = Path(source_file_str) | ||
| if not source_file.exists(): | ||
| return | ||
|
|
||
| parsed = parse_instinct_file(source_file.read_text(encoding="utf-8")) | ||
| if not any(i.get('id') == instinct_id for i in parsed): | ||
| # Target not found in a successful parse - either already removed, or the | ||
| # file is malformed/foreign content. Either way, don't touch it: a parse | ||
| # failure must never be treated as proof the file is safe to delete. | ||
| return | ||
|
|
||
| remaining = [i for i in parsed if i.get('id') != instinct_id] | ||
|
|
||
| if not remaining: | ||
| source_file.unlink() | ||
| return | ||
|
|
||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| blocks = [] | ||
| for inst in remaining: | ||
| block = "---\n" | ||
| for key, value in inst.items(): | ||
| if key == 'content' or key.startswith('_'): | ||
| continue | ||
| block += f"{key}: {_yaml_quote(value) if key == 'trigger' else value}\n" | ||
| block += "---\n\n" | ||
| block += inst.get('content', '') + "\n" | ||
|
Comment on lines
+1346
to
+1351
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Use YAML-safe serialization for every retained field. Only As per coding guidelines, external data must be treated as untrusted and schema-based validation/serialization should be used. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| blocks.append(block) | ||
| source_file.write_text("\n".join(blocks), encoding="utf-8") | ||
|
Comment on lines
+1339
to
+1353
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Promotion is not transactional across global and source files. The current sequence can lose remaining source data or leave stale project copies after a crash or filesystem error.
As per coding guidelines, errors must be handled explicitly at every level. 📍 Affects 1 file
🤖 Prompt for AI AgentsSource: Coding guidelines 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Make source cleanup atomic.
As per coding guidelines, errors must be handled explicitly at every level. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
|
|
||
| def cmd_promote(args) -> int: | ||
| """Promote project-scoped instincts to global scope.""" | ||
| project = detect_project() | ||
|
|
@@ -1376,6 +1415,11 @@ def _promote_specific(project: dict, instinct_id: str, force: bool, dry_run: boo | |
| output_content += target.get('content', '') + "\n" | ||
|
|
||
| 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" Saved to: {output_file}") | ||
| return 0 | ||
|
|
@@ -1449,6 +1493,12 @@ def _promote_auto(project: dict, force: bool, dry_run: bool) -> int: | |
| output_content += inst.get('content', '') + "\n" | ||
|
|
||
| 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 | ||
|
|
||
| print(f"\nPromoted {promoted} instincts to global scope.") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Require a complete, lossless parse before modifying the source.
Checking that one parsed block has the target ID is insufficient:
parse_instinct_file()filters out blocks withoutidand tolerates malformed content. A file containing the target plus unreadable/foreign content can therefore pass this guard;remainingmay be empty and Line 1340 can delete the whole file, or the rewrite can drop those blocks. Require strict validation of every source block, or preserve unknown/raw blocks during removal.As per coding guidelines, external data must be treated as untrusted and schema-validated before processing.
🤖 Prompt for AI Agents
Source: Coding guidelines