@deftai/directive-content 0.59.0 → 0.60.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 (184) hide show
  1. package/.githooks/pre-push +10 -9
  2. package/Taskfile.yml +48 -58
  3. package/UPGRADING.md +1 -1
  4. package/docs/assets/directive-lifecycle-diagram.png +0 -0
  5. package/docs/directive-lifecycle.md +73 -0
  6. package/docs/getting-started.md +5 -1
  7. package/package.json +3 -3
  8. package/packs/skills/skills-pack-0.1.json +22 -22
  9. package/scm/github.md +20 -2
  10. package/tasks/change.yml +16 -31
  11. package/tasks/ci.yml +8 -0
  12. package/tasks/commit.yml +12 -19
  13. package/tasks/core.yml +10 -0
  14. package/tasks/engine.yml +42 -0
  15. package/tasks/framework.yml +3 -0
  16. package/tasks/install.yml +20 -19
  17. package/tasks/migrate.yml +26 -15
  18. package/tasks/project.yml +16 -0
  19. package/tasks/toolchain.yml +15 -5
  20. package/tasks/vbrief.yml +4 -3
  21. package/tasks/verify.yml +12 -14
  22. package/scripts/_agents_md.py +0 -494
  23. package/scripts/_cache_fetch.py +0 -635
  24. package/scripts/_cache_quota.py +0 -529
  25. package/scripts/_cache_refresh.py +0 -163
  26. package/scripts/_cache_validate.py +0 -209
  27. package/scripts/_content_root.py +0 -42
  28. package/scripts/_doctor_state.py +0 -277
  29. package/scripts/_event_detect.py +0 -305
  30. package/scripts/_events.py +0 -514
  31. package/scripts/_lifecycle_hygiene.py +0 -568
  32. package/scripts/_pathspec.py +0 -91
  33. package/scripts/_policy_show_cli.py +0 -266
  34. package/scripts/_precutover.py +0 -92
  35. package/scripts/_project_context.py +0 -224
  36. package/scripts/_project_definition_io.py +0 -164
  37. package/scripts/_relocate_snapshot.py +0 -209
  38. package/scripts/_relocate_states.py +0 -343
  39. package/scripts/_resolve_preflight_path.py +0 -152
  40. package/scripts/_safe_subprocess.py +0 -167
  41. package/scripts/_session_start_hook.py +0 -205
  42. package/scripts/_sor_gate_diff.py +0 -365
  43. package/scripts/_stdio_utf8.py +0 -59
  44. package/scripts/_triage_bootstrap_gitignore.py +0 -904
  45. package/scripts/_triage_classify_cli.py +0 -122
  46. package/scripts/_triage_queue_cli.py +0 -625
  47. package/scripts/_triage_scope_cli.py +0 -343
  48. package/scripts/_triage_scope_drift_cli.py +0 -121
  49. package/scripts/_triage_scope_ignores.py +0 -286
  50. package/scripts/_triage_scope_milestone.py +0 -432
  51. package/scripts/_triage_scope_mutations.py +0 -337
  52. package/scripts/_triage_scope_renderers.py +0 -207
  53. package/scripts/_triage_smoketest_stages.py +0 -674
  54. package/scripts/_triage_subscribe_cli.py +0 -140
  55. package/scripts/_triage_welcome_cli.py +0 -421
  56. package/scripts/_vbrief_build.py +0 -239
  57. package/scripts/_vbrief_fidelity.py +0 -479
  58. package/scripts/_vbrief_legacy.py +0 -589
  59. package/scripts/_vbrief_reconciliation.py +0 -883
  60. package/scripts/_vbrief_routing.py +0 -277
  61. package/scripts/_vbrief_safety.py +0 -778
  62. package/scripts/_vbrief_sources.py +0 -312
  63. package/scripts/_vbrief_speckit.py +0 -262
  64. package/scripts/_vbrief_story_quality.py +0 -353
  65. package/scripts/_vbrief_validation.py +0 -299
  66. package/scripts/build_dist.py +0 -412
  67. package/scripts/cache.py +0 -1078
  68. package/scripts/cache_scanner.py +0 -745
  69. package/scripts/candidates_log.py +0 -432
  70. package/scripts/capacity_backfill.py +0 -680
  71. package/scripts/capacity_show.py +0 -653
  72. package/scripts/ci_local.py +0 -689
  73. package/scripts/code_structure_validate.py +0 -765
  74. package/scripts/codebase_default_extractor.py +0 -495
  75. package/scripts/codebase_map.py +0 -304
  76. package/scripts/codebase_map_fresh.py +0 -104
  77. package/scripts/codebase_projection_registry.py +0 -94
  78. package/scripts/codebase_provider.py +0 -582
  79. package/scripts/doctor.py +0 -2552
  80. package/scripts/framework_commands.py +0 -505
  81. package/scripts/gh_rest.py +0 -882
  82. package/scripts/github_auth_modes.py +0 -437
  83. package/scripts/github_body.py +0 -292
  84. package/scripts/ip_risk.py +0 -531
  85. package/scripts/issue_emit.py +0 -670
  86. package/scripts/issue_ingest.py +0 -1064
  87. package/scripts/migrate_preflight.py +0 -418
  88. package/scripts/migrate_vbrief.py +0 -2677
  89. package/scripts/monitor_pr.py +0 -401
  90. package/scripts/pack_migrate_lessons.py +0 -336
  91. package/scripts/pack_migrate_patterns.py +0 -254
  92. package/scripts/pack_migrate_rules.py +0 -350
  93. package/scripts/pack_migrate_skills.py +0 -423
  94. package/scripts/pack_migrate_strategies.py +0 -311
  95. package/scripts/pack_migrate_swarm_spec.py +0 -250
  96. package/scripts/pack_render.py +0 -434
  97. package/scripts/packs_slice.py +0 -712
  98. package/scripts/platform_capabilities.py +0 -336
  99. package/scripts/policy.py +0 -2826
  100. package/scripts/policy_set.py +0 -324
  101. package/scripts/pr_check_closing_keywords.py +0 -524
  102. package/scripts/pr_check_protected_issues.py +0 -267
  103. package/scripts/pr_merge_readiness.py +0 -1004
  104. package/scripts/pr_wait_mergeable.py +0 -669
  105. package/scripts/prd_render.py +0 -159
  106. package/scripts/preflight_architecture_sor.py +0 -974
  107. package/scripts/preflight_branch.py +0 -289
  108. package/scripts/preflight_cache.py +0 -974
  109. package/scripts/preflight_gh.py +0 -721
  110. package/scripts/preflight_implementation.py +0 -272
  111. package/scripts/preflight_story_start.py +0 -838
  112. package/scripts/preflight_wip_cap.py +0 -149
  113. package/scripts/probe_session.py +0 -545
  114. package/scripts/project_render.py +0 -293
  115. package/scripts/quarantine_ext.py +0 -237
  116. package/scripts/reconcile_issues.py +0 -1442
  117. package/scripts/refresh-path.ps1 +0 -107
  118. package/scripts/release.py +0 -2030
  119. package/scripts/release_e2e.py +0 -1011
  120. package/scripts/release_publish.py +0 -486
  121. package/scripts/release_rollback.py +0 -980
  122. package/scripts/relocate.py +0 -1034
  123. package/scripts/resolve_changelog_unreleased.py +0 -667
  124. package/scripts/resolve_version.py +0 -490
  125. package/scripts/resume_conditions.py +0 -706
  126. package/scripts/ritual_sentinel.py +0 -609
  127. package/scripts/roadmap_render.py +0 -635
  128. package/scripts/rule_ownership_lint.py +0 -325
  129. package/scripts/scm.py +0 -591
  130. package/scripts/scope_audit_log.py +0 -387
  131. package/scripts/scope_decompose.py +0 -654
  132. package/scripts/scope_demote.py +0 -509
  133. package/scripts/scope_lifecycle.py +0 -1126
  134. package/scripts/scope_undo.py +0 -772
  135. package/scripts/session_start.py +0 -406
  136. package/scripts/setup_ghx.py +0 -339
  137. package/scripts/setup_windows.ps1 +0 -220
  138. package/scripts/slice_audit.py +0 -585
  139. package/scripts/slice_record.py +0 -530
  140. package/scripts/slice_record_existing.py +0 -692
  141. package/scripts/slug_normalize.py +0 -178
  142. package/scripts/spec_render.py +0 -477
  143. package/scripts/spec_validate.py +0 -238
  144. package/scripts/subagent_monitor.py +0 -658
  145. package/scripts/swarm_complete_cohort.py +0 -644
  146. package/scripts/swarm_launch.py +0 -1206
  147. package/scripts/swarm_readiness.py +0 -554
  148. package/scripts/swarm_verify_review_clean.py +0 -438
  149. package/scripts/swarm_worktrees.py +0 -497
  150. package/scripts/toolchain-check.py +0 -52
  151. package/scripts/triage_actions.py +0 -871
  152. package/scripts/triage_bootstrap.py +0 -1153
  153. package/scripts/triage_bulk.py +0 -630
  154. package/scripts/triage_classify.py +0 -932
  155. package/scripts/triage_help.py +0 -1685
  156. package/scripts/triage_queue.py +0 -1944
  157. package/scripts/triage_reconcile.py +0 -581
  158. package/scripts/triage_refresh.py +0 -643
  159. package/scripts/triage_scope.py +0 -999
  160. package/scripts/triage_scope_drift.py +0 -575
  161. package/scripts/triage_smoketest.py +0 -396
  162. package/scripts/triage_subscribe.py +0 -399
  163. package/scripts/triage_summary.py +0 -1011
  164. package/scripts/triage_welcome.py +0 -1178
  165. package/scripts/ts_check_lane.py +0 -86
  166. package/scripts/validate-links.py +0 -64
  167. package/scripts/validate_strategy_output.py +0 -212
  168. package/scripts/vbrief_activate.py +0 -228
  169. package/scripts/vbrief_migrate_conformance.py +0 -368
  170. package/scripts/vbrief_reconcile_graph.py +0 -306
  171. package/scripts/vbrief_reconcile_labels.py +0 -460
  172. package/scripts/vbrief_reconcile_umbrellas.py +0 -741
  173. package/scripts/vbrief_validate.py +0 -1144
  174. package/scripts/verify-stubs.py +0 -61
  175. package/scripts/verify_capacity.py +0 -160
  176. package/scripts/verify_encoding.py +0 -699
  177. package/scripts/verify_hooks_installed.py +0 -206
  178. package/scripts/verify_investigation.py +0 -360
  179. package/scripts/verify_judgment_gates.py +0 -827
  180. package/scripts/verify_no_task_runtime.py +0 -171
  181. package/scripts/verify_scm_boundary.py +0 -509
  182. package/scripts/verify_session_ritual.py +0 -389
  183. package/scripts/verify_tools.py +0 -426
  184. package/scripts/verify_vbrief_conformance.py +0 -478
@@ -1,293 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- project_render.py — Regenerate PROJECT-DEFINITION.vbrief.json from lifecycle folders.
4
-
5
- Deterministic layer (RFC #309, Decision D14):
6
- - Scans all lifecycle folders (proposed/, pending/, active/, completed/, cancelled/)
7
- - Updates the items registry with scope entries (title, status, file path, references)
8
- - Timestamps freshness (vBRIEFInfo.updated)
9
- - Flags narratives that may be stale based on completed scope topics
10
- - Creates skeleton PROJECT-DEFINITION.vbrief.json if none exists
11
-
12
- Agent-assisted layer (documented convention, not implemented as code):
13
- During sync or refinement sessions, the agent reviews flagged narratives and
14
- proposes updates to project identity (overview, capabilities, risks, tech stack)
15
- based on completed work. The user approves -- never fully automatic for content
16
- requiring judgment.
17
-
18
- Workflow:
19
- 1. Run `task project:render` to refresh the items registry and staleness flags.
20
- 2. The agent reads staleness_flags from plan.metadata.staleness_flags.
21
- 3. For each flagged narrative, the agent drafts a proposed update reflecting
22
- the completed scopes (e.g. if a "tech stack" scope completed, update the
23
- TechStack narrative with the new technology choices).
24
- 4. The user reviews and approves each narrative change.
25
- 5. The agent writes approved changes back to PROJECT-DEFINITION.vbrief.json.
26
-
27
- Usage:
28
- uv run python scripts/project_render.py [vbrief_dir]
29
-
30
- vbrief_dir — path to vbrief/ directory (default: ./vbrief)
31
-
32
- Exit codes:
33
- 0 — rendered successfully
34
- 1 — error occurred
35
- 2 — usage error
36
- """
37
-
38
- import json
39
- import re
40
- import sys
41
- from datetime import UTC, datetime
42
- from pathlib import Path
43
-
44
- # Make sibling scripts importable both when run as __main__ and when imported
45
- # by tests that pre-populate sys.path with the ``scripts/`` directory.
46
- sys.path.insert(0, str(Path(__file__).resolve().parent))
47
-
48
- # UTF-8 stdout guard (#540).
49
- from _stdio_utf8 import reconfigure_stdio # noqa: E402
50
-
51
- reconfigure_stdio()
52
-
53
- from _vbrief_build import ( # noqa: E402
54
- EMITTED_VBRIEF_VERSION as _EMITTED_VBRIEF_VERSION,
55
- )
56
-
57
- LIFECYCLE_FOLDERS = ("proposed", "pending", "active", "completed", "cancelled")
58
-
59
- # Keys intentionally match scripts/vbrief_validate.py PROJECT_DEF_EXPECTED_NARRATIVES
60
- # after case-folding: "overview" and "tech stack" are required by D3 (#405).
61
- # Keep the "tech stack" key exactly as-is (lowercase, space-separated) so
62
- # `task project:render` skeletons pass `task vbrief:validate` immediately.
63
- SKELETON_NARRATIVES = {
64
- "Overview": "",
65
- "tech stack": "",
66
- "Architecture": "",
67
- "RisksAndUnknowns": "",
68
- "Configuration": "",
69
- }
70
-
71
-
72
- def _split_camel(name: str) -> list[str]:
73
- """Split a camelCase or PascalCase string into lowercase words.
74
-
75
- >>> _split_camel("TechStack")
76
- ['tech', 'stack']
77
- >>> _split_camel("RisksAndUnknowns")
78
- ['risks', 'and', 'unknowns']
79
- """
80
- parts = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
81
- return [w.lower() for w in parts.split()]
82
-
83
-
84
- def scan_lifecycle_folders(vbrief_dir: Path) -> list[dict]:
85
- """Scan all lifecycle folders for *.vbrief.json files and return items.
86
-
87
- Scans folders in a fixed order (proposed, pending, active, completed,
88
- cancelled) and files alphabetically within each folder, producing
89
- deterministic output for the same folder contents.
90
- """
91
- items: list[dict] = []
92
- for folder_name in LIFECYCLE_FOLDERS:
93
- folder = vbrief_dir / folder_name
94
- if not folder.is_dir():
95
- continue
96
- for vbrief_file in sorted(folder.glob("*.vbrief.json")):
97
- try:
98
- with open(vbrief_file, encoding="utf-8") as fh:
99
- data = json.load(fh)
100
- plan = data.get("plan", {})
101
- title = plan.get("title", vbrief_file.stem)
102
- status = plan.get("status", folder_name)
103
- references = plan.get("references", [])
104
-
105
- item: dict = {
106
- "id": vbrief_file.stem.replace(".vbrief", ""),
107
- "title": title,
108
- "status": status,
109
- "metadata": {
110
- "source_path": f"{folder_name}/{vbrief_file.name}",
111
- "lifecycle_folder": folder_name,
112
- },
113
- }
114
- if references:
115
- item["metadata"]["references"] = references
116
- items.append(item)
117
- except (json.JSONDecodeError, OSError):
118
- items.append(
119
- {
120
- "id": vbrief_file.stem.replace(".vbrief", ""),
121
- "title": f"[unreadable] {vbrief_file.name}",
122
- "status": "draft",
123
- "metadata": {
124
- "source_path": f"{folder_name}/{vbrief_file.name}",
125
- "lifecycle_folder": folder_name,
126
- "error": "Failed to read or parse file",
127
- },
128
- }
129
- )
130
- return items
131
-
132
-
133
- def flag_stale_narratives(
134
- narratives: dict[str, str],
135
- completed_items: list[dict],
136
- ) -> list[str]:
137
- """Flag narratives that may need review based on completed scope topics.
138
-
139
- Algorithm (deterministic):
140
- 1. Split each narrative key into words (camelCase-aware).
141
- 2. For each completed scope, extract title words.
142
- 3. If any narrative-key word (>3 chars) appears in a completed scope title,
143
- flag that narrative with the matching scope.
144
- 4. If >=3 completed scopes exist and no specific flags fired, emit a general
145
- review recommendation.
146
-
147
- Returns a sorted list of staleness warning strings.
148
- """
149
- if not completed_items or not narratives:
150
- if completed_items and len(completed_items) >= 3:
151
- return [
152
- f"{len(completed_items)} scopes completed since last narrative update"
153
- " -- review recommended"
154
- ]
155
- return []
156
-
157
- flags: list[str] = []
158
- flagged_narratives: set[str] = set()
159
-
160
- for narrative_key in sorted(narratives.keys()):
161
- key_words = {w for w in _split_camel(narrative_key) if len(w) > 3}
162
- if not key_words:
163
- continue
164
- for item in completed_items:
165
- title_lower = item.get("title", "").lower()
166
- title_words = set(re.split(r"\W+", title_lower))
167
- overlap = key_words & title_words
168
- if overlap:
169
- flags.append(
170
- f"Narrative '{narrative_key}' may be stale: "
171
- f"completed scope '{item.get('title', '')}' "
172
- f"shares topics ({', '.join(sorted(overlap))})"
173
- )
174
- flagged_narratives.add(narrative_key)
175
-
176
- # General flag if many completed scopes but no specific matches
177
- if len(completed_items) >= 3 and not flagged_narratives:
178
- flags.append(
179
- f"{len(completed_items)} scopes completed since last narrative update"
180
- " -- review recommended"
181
- )
182
-
183
- return sorted(flags)
184
-
185
-
186
- def create_skeleton(items: list[dict], now: str) -> dict:
187
- """Create a skeleton PROJECT-DEFINITION.vbrief.json structure."""
188
- completed_items = [i for i in items if i.get("status") == "completed"]
189
- staleness_flags = flag_stale_narratives(dict(SKELETON_NARRATIVES), completed_items)
190
-
191
- return {
192
- "vBRIEFInfo": {
193
- # #533: match the migrator's emitted version so skeletons
194
- # produced by ``task project:render`` round-trip through the
195
- # validator during the v0.6 transition. Sourced from the
196
- # shared constant in _vbrief_build so a future bump lands in
197
- # one place.
198
- "version": _EMITTED_VBRIEF_VERSION,
199
- "description": "Project definition -- synthesized gestalt of the project",
200
- "created": now,
201
- "updated": now,
202
- },
203
- "plan": {
204
- "title": "PROJECT-DEFINITION",
205
- "status": "running",
206
- "narratives": dict(SKELETON_NARRATIVES),
207
- "items": items,
208
- "metadata": {
209
- "staleness_flags": staleness_flags,
210
- },
211
- },
212
- }
213
-
214
-
215
- def render_project_definition(vbrief_dir: str) -> tuple[bool, str]:
216
- """Regenerate PROJECT-DEFINITION.vbrief.json from lifecycle folder contents.
217
-
218
- Returns:
219
- (True, success_message) on success.
220
- (False, error_message) on failure.
221
- """
222
- vbrief_path = Path(vbrief_dir)
223
- project_def_path = vbrief_path / "PROJECT-DEFINITION.vbrief.json"
224
-
225
- # Scan lifecycle folders (handles missing folders gracefully)
226
- items = scan_lifecycle_folders(vbrief_path)
227
-
228
- now = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
229
- created_new = not project_def_path.exists()
230
-
231
- if project_def_path.exists():
232
- # Update existing PROJECT-DEFINITION
233
- try:
234
- with open(project_def_path, encoding="utf-8") as fh:
235
- project_def = json.load(fh)
236
- except (json.JSONDecodeError, OSError) as exc:
237
- return False, f"✗ Failed to read {project_def_path}: {exc}"
238
-
239
- plan = project_def.get("plan", {})
240
-
241
- # Update items registry (deterministic)
242
- plan["items"] = items
243
-
244
- # Timestamp freshness
245
- project_def.setdefault("vBRIEFInfo", {})
246
- project_def["vBRIEFInfo"]["updated"] = now
247
-
248
- # Flag stale narratives
249
- narratives = plan.get("narratives", {})
250
- completed_items = [i for i in items if i.get("status") == "completed"]
251
- flags = flag_stale_narratives(narratives, completed_items)
252
- plan.setdefault("metadata", {})
253
- plan["metadata"]["staleness_flags"] = flags
254
-
255
- project_def["plan"] = plan
256
- else:
257
- # Create skeleton
258
- project_def = create_skeleton(items, now)
259
-
260
- # Ensure parent directory exists
261
- project_def_path.parent.mkdir(parents=True, exist_ok=True)
262
-
263
- # Write deterministic output
264
- with open(project_def_path, "w", encoding="utf-8") as fh:
265
- json.dump(project_def, fh, indent=2, ensure_ascii=False)
266
- fh.write("\n")
267
-
268
- # Report
269
- item_count = len(items)
270
- flag_count = len(project_def["plan"].get("metadata", {}).get("staleness_flags", []))
271
- action = "created" if created_new else "updated"
272
-
273
- parts = [f"✓ PROJECT-DEFINITION.vbrief.json {action} ({item_count} scope items)"]
274
- if flag_count:
275
- parts.append(f"⚠ {flag_count} staleness flag(s) -- agent review recommended")
276
-
277
- return True, "\n".join(parts)
278
-
279
-
280
- def main() -> int:
281
- if len(sys.argv) > 2:
282
- print("Usage: project_render.py [vbrief_dir]", file=sys.stderr)
283
- return 2
284
-
285
- vbrief_dir = sys.argv[1] if len(sys.argv) == 2 else "vbrief"
286
-
287
- ok, message = render_project_definition(vbrief_dir)
288
- print(message)
289
- return 0 if ok else 1
290
-
291
-
292
- if __name__ == "__main__":
293
- sys.exit(main())
@@ -1,237 +0,0 @@
1
- #!/usr/bin/env python3
2
- r"""quarantine_ext.py -- prompt-injection quarantine for cached issue bodies (#583).
3
-
4
- Public surface
5
- --------------
6
-
7
- ``quarantine_body(raw_md: str) -> str``
8
- Return ``raw_md`` with any injection-shaped sections wrapped in
9
- ``\`\`\`quarantined`` fenced code blocks. Idempotent: input already wrapped
10
- in ``quarantined`` fences is left unchanged.
11
-
12
- Background
13
- ----------
14
-
15
- Issue bodies on GitHub frequently contain imperative-shaped markdown that
16
- looks like agent instructions (``# STEP 1``, ``## TASK:``, ``IMPORTANT:`` /
17
- ``MUST`` headings, ``SYSTEM:`` directives, etc.). When a downstream agent
18
- reads a cached issue body verbatim, the text is *data*, not *instructions* --
19
- but a careless prompt template can splice the body directly into the agent's
20
- turn payload, allowing a hostile issue author to redirect the agent.
21
-
22
- #583 codified the mitigation: the cache layer wraps suspicious sections in a
23
- ``\`\`\`quarantined`` fenced code block so downstream consumers can detect and
24
- either strip the section or emit a clear `do not follow these instructions`
25
- preamble around it. The fence label is intentionally a non-standard
26
- language-id so it is a syntactic marker, not a renderable hint.
27
-
28
- Heuristic
29
- ---------
30
-
31
- A markdown heading line (``^#{1,6} +``) is considered *suspicious* when it
32
- contains one of the imperative tokens listed in :data:`SUSPICIOUS_TOKENS`
33
- (case-insensitive, word-boundary scoped). Every line from a suspicious
34
- heading down to (but not including) the next heading -- or end of document --
35
- is treated as the suspicious section and wrapped.
36
-
37
- Non-heading injection patterns (e.g. ``IMPORTANT:`` or ``SYSTEM:`` on a
38
- plain prose line) also trigger wrapping of that line so a one-shot directive
39
- embedded in body prose is still flagged.
40
-
41
- The heuristic is intentionally permissive (false-positives wrap benign
42
- ``## Steps to reproduce`` sections in a quarantined fence). The downstream
43
- display layer is responsible for unwrapping legitimate sections; the cost
44
- of a false positive is one extra fence in the rendered output, while the
45
- cost of a false negative is an exfiltrated agent turn.
46
-
47
- CLI
48
- ---
49
-
50
- The module is callable as a script:
51
-
52
- python scripts/quarantine_ext.py [<input-file>]
53
-
54
- Reads ``<input-file>`` if given, otherwise stdin, and writes the quarantined
55
- markdown to stdout. Useful for ad-hoc inspection.
56
- """
57
-
58
- from __future__ import annotations
59
-
60
- import re
61
- import sys
62
- from pathlib import Path
63
-
64
- # Imperative tokens that mark a heading or line as injection-shaped. The set
65
- # is curated against the recurrence record in #583 plus the canonical
66
- # agent-prompt vocabulary used by Warp / Oz / Claude / OpenAI tool surfaces.
67
- # Word-boundary scoped so the substring ``step`` inside ``stepladder`` does
68
- # NOT trigger.
69
- SUSPICIOUS_TOKENS: tuple[str, ...] = (
70
- "STEP",
71
- "TASK:",
72
- "TASK ",
73
- "IMPORTANT:",
74
- "IMPORTANT ",
75
- "MUST",
76
- "SYSTEM:",
77
- "SYSTEM ",
78
- "AGENT:",
79
- "AGENT ",
80
- "ASSISTANT:",
81
- "USER:",
82
- "INSTRUCTION:",
83
- "INSTRUCTIONS:",
84
- "TOOL:",
85
- "FUNCTION:",
86
- "PROMPT:",
87
- "OVERRIDE:",
88
- "IGNORE PREVIOUS",
89
- "DISREGARD PREVIOUS",
90
- "FORGET PREVIOUS",
91
- "ROLE:",
92
- "DIRECTIVE:",
93
- )
94
-
95
- # Regex source of truth -- compiled once. Word-boundary on each side of the
96
- # token, except for tokens that already include trailing punctuation
97
- # (``TASK:`` etc.) where the punctuation acts as the boundary.
98
- _TOKEN_PATTERNS = []
99
- for _tok in SUSPICIOUS_TOKENS:
100
- if _tok.endswith((":", " ")):
101
- # punctuation-anchored -- no trailing \b (the colon/space is the boundary)
102
- _TOKEN_PATTERNS.append(r"\b" + re.escape(_tok))
103
- else:
104
- _TOKEN_PATTERNS.append(r"\b" + re.escape(_tok) + r"\b")
105
- _TOKEN_RE = re.compile("|".join(_TOKEN_PATTERNS), re.IGNORECASE)
106
-
107
- # Heading detector: 1-6 hashes followed by at least one space. Setext-style
108
- # headings (=== / ---) are intentionally not detected because they require
109
- # multi-line lookahead and are vanishingly rare in GitHub-flavoured-markdown
110
- # issue bodies.
111
- _HEADING_RE = re.compile(r"^(#{1,6})\s+(.*\S.*)$")
112
-
113
- # A code-fence delimiter line. Tracks whether we are inside an existing
114
- # code block so heading-shaped text inside ```text``` is not re-quarantined.
115
- _FENCE_RE = re.compile(r"^(```|~~~)")
116
-
117
- QUARANTINE_FENCE_OPEN = "```quarantined"
118
- QUARANTINE_FENCE_CLOSE = "```"
119
-
120
-
121
- def _is_suspicious(line: str) -> bool:
122
- return bool(_TOKEN_RE.search(line))
123
-
124
-
125
- def _is_heading(line: str) -> bool:
126
- return bool(_HEADING_RE.match(line))
127
-
128
-
129
- def quarantine_body(raw_md: str) -> str:
130
- r"""Wrap injection-shaped sections in ``\`\`\`quarantined`` fences.
131
-
132
- Args:
133
- raw_md: Raw markdown body (e.g. the rendered text of a GitHub issue
134
- body fetched via ``gh issue view --json body``).
135
-
136
- Returns:
137
- The same markdown with suspicious sections wrapped. If no sections
138
- match, the input is returned unchanged (modulo trailing-newline
139
- normalization).
140
-
141
- The function is idempotent: re-running on already-quarantined text is a
142
- no-op because the existing ``\`\`\`quarantined`` fence is recognised as
143
- a code block and its contents are skipped.
144
- """
145
- if not raw_md:
146
- return raw_md
147
-
148
- lines = raw_md.splitlines()
149
- out: list[str] = []
150
- i = 0
151
- in_fence: str | None = None # the fence delimiter we are inside, if any
152
-
153
- while i < len(lines):
154
- line = lines[i]
155
-
156
- # Track existing fenced code blocks so we don't re-wrap them.
157
- # ``in_fence`` records the opening delimiter; we only close on a
158
- # matching delimiter (Greptile P1: previously closed on the
159
- # current line's delim, which let a ``~~~`` line close an open
160
- # ``\`\`\`` fence and reopen a new one, leaving suspicious headings
161
- # after that point unquarantined).
162
- fence_match = _FENCE_RE.match(line)
163
- if fence_match:
164
- delim = fence_match.group(1)
165
- if in_fence is None:
166
- in_fence = delim
167
- elif line.startswith(in_fence):
168
- in_fence = None
169
- out.append(line)
170
- i += 1
171
- continue
172
- if in_fence is not None:
173
- out.append(line)
174
- i += 1
175
- continue
176
-
177
- # Suspicious heading: capture from this line to (but not including)
178
- # the next heading -- regardless of whether the next heading is also
179
- # suspicious. The next iteration will re-wrap the next section if
180
- # needed.
181
- if _is_heading(line) and _is_suspicious(line):
182
- section_end = i + 1
183
- while section_end < len(lines):
184
- nxt = lines[section_end]
185
- if _FENCE_RE.match(nxt):
186
- # do not split a quarantined block across an unbalanced
187
- # fence -- consume the entire interior. Both ``\`\`\``
188
- # and ``~~~`` are 3-char delimiters; we slice the same
189
- # prefix length and match the literal opener (Greptile
190
- # P3: dead-conditional cleanup).
191
- section_end += 1
192
- nested = nxt[:3]
193
- while section_end < len(lines) and not lines[
194
- section_end
195
- ].startswith(nested):
196
- section_end += 1
197
- section_end += 1 # consume the closer
198
- continue
199
- if _is_heading(nxt):
200
- break
201
- section_end += 1
202
- out.append(QUARANTINE_FENCE_OPEN)
203
- out.extend(lines[i:section_end])
204
- out.append(QUARANTINE_FENCE_CLOSE)
205
- i = section_end
206
- continue
207
-
208
- # Suspicious non-heading line: wrap just that line.
209
- if _is_suspicious(line):
210
- out.append(QUARANTINE_FENCE_OPEN)
211
- out.append(line)
212
- out.append(QUARANTINE_FENCE_CLOSE)
213
- i += 1
214
- continue
215
-
216
- out.append(line)
217
- i += 1
218
-
219
- # Preserve trailing newline behaviour of the input. If the input ends
220
- # with a newline, splitlines() drops it; re-add for round-trip safety.
221
- suffix = "\n" if raw_md.endswith("\n") else ""
222
- return "\n".join(out) + suffix
223
-
224
-
225
- def main(argv: list[str] | None = None) -> int:
226
- """CLI entry point. Reads input file (or stdin) and emits quarantined md."""
227
- argv = list(argv if argv is not None else sys.argv[1:])
228
- if argv and argv[0] in {"-h", "--help"}:
229
- print(__doc__ or "")
230
- return 0
231
- text = Path(argv[0]).read_text(encoding="utf-8") if argv else sys.stdin.read()
232
- sys.stdout.write(quarantine_body(text))
233
- return 0
234
-
235
-
236
- if __name__ == "__main__":
237
- raise SystemExit(main())