cowork-flow 0.0.37 → 0.0.39

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 (111) hide show
  1. package/README.md +27 -14
  2. package/package.json +1 -1
  3. package/src/commands/init.js +39 -28
  4. package/src/commands/sync.js +2 -2
  5. package/src/lib/asset-plan.js +127 -0
  6. package/src/lib/copy-template.js +101 -141
  7. package/src/lib/host-assets.js +231 -0
  8. package/src/lib/plan-applier.js +329 -0
  9. package/src/lib/platforms.js +15 -76
  10. package/src/lib/skill-registry.js +503 -0
  11. package/template/.claude/agents/cowork-check.md +10 -1
  12. package/template/.claude/agents/cowork-implement.md +11 -2
  13. package/template/.claude/hooks/inject-workflow-state.py +51 -254
  14. package/template/.codex/agents/cowork-check.toml +12 -3
  15. package/template/.codex/agents/cowork-implement.toml +11 -2
  16. package/template/.codex/agents/default.toml +3 -2
  17. package/template/.codex/agents/explorer.toml +3 -2
  18. package/template/.codex/agents/worker.toml +2 -1
  19. package/template/.codex/hooks/inject-workflow-state.py +46 -265
  20. package/template/.cowork-flow/.version +1 -1
  21. package/template/.cowork-flow/scripts/application/__init__.py +1 -0
  22. package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
  23. package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
  24. package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
  25. package/template/.cowork-flow/scripts/application/task_context.py +765 -0
  26. package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
  27. package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
  28. package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
  29. package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
  30. package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
  31. package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
  32. package/template/.cowork-flow/scripts/commands/task.py +535 -1953
  33. package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
  34. package/template/.cowork-flow/scripts/commands/task_context_commands.py +240 -0
  35. package/template/.cowork-flow/scripts/commands/task_create_command.py +164 -0
  36. package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
  37. package/template/.cowork-flow/scripts/commands/task_parser.py +364 -0
  38. package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
  39. package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
  40. package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
  41. package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
  42. package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
  43. package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
  44. package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
  45. package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
  46. package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
  47. package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
  48. package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
  49. package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
  50. package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
  51. package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
  52. package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
  53. package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
  54. package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
  55. package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
  56. package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
  57. package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
  58. package/template/.cowork-flow/scripts/common/task/active_task.py +93 -135
  59. package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
  60. package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
  61. package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
  62. package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
  63. package/template/.cowork-flow/scripts/run.py +24 -0
  64. package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  65. package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  66. package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
  67. package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
  68. package/template/.cowork-flow/spec/protocols/review.md +34 -0
  69. package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
  70. package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
  71. package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
  72. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  73. package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
  74. package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
  75. package/template/.cowork-flow/spec/runtime/index.md +8 -0
  76. package/template/.cowork-flow/spec/runtime/rules.json +61 -10
  77. package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
  78. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
  79. package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  80. package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
  81. package/template/.cowork-flow/workflow.md +18 -13
  82. package/template/.opencode/agents/cowork-check.md +11 -0
  83. package/template/.opencode/agents/cowork-implement.md +16 -1
  84. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  85. package/template/.zcode/hooks/inject-context.js +3 -85
  86. package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
  87. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  88. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  89. package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
  90. package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  91. package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
  92. package/template/.zcode/scaffold/AGENTS.md +18 -8
  93. package/template/AGENTS.md +1 -1
  94. package/template/skills/batch-mode/SKILL.md +140 -76
  95. package/template/skills/brainstorming/SKILL.md +2 -2
  96. package/template/skills/cowork-flow/SKILL.md +42 -0
  97. package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
  98. package/template/skills/party-mode/SKILL.md +48 -119
  99. package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
  100. package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
  101. package/template/skills/before-dev/SKILL.md +0 -41
  102. package/template/skills/check/SKILL.md +0 -53
  103. package/template/skills/check/simplification-guide.md +0 -47
  104. package/template/skills/continue/SKILL.md +0 -29
  105. package/template/skills/doubt-review/SKILL.md +0 -118
  106. package/template/skills/finish-work/SKILL.md +0 -32
  107. package/template/skills/party-mode-v2/SKILL.md +0 -75
  108. package/template/skills/start/SKILL.md +0 -78
  109. package/template/skills/tdd/SKILL.md +0 -74
  110. package/template/skills/update-spec/SKILL.md +0 -39
  111. package/template/skills/using-cowork-flow/SKILL.md +0 -80
@@ -91,11 +91,11 @@ SETUP_FAILURE_MARKERS = (
91
91
  def validate_tdd_evidence(task_dir: Path) -> list[dict]:
92
92
  """Return TDD evidence violations for a task directory."""
93
93
  task_dir = Path(task_dir)
94
- prd_text = _read_text(task_dir / "decision-anchor.md")
94
+ decision_anchor_text = _read_text(task_dir / "decision-anchor.md")
95
95
  evidence_path = task_dir / EVIDENCE_FILE
96
96
 
97
97
  if not evidence_path.is_file():
98
- if _task_requires_tdd(prd_text):
98
+ if _task_requires_tdd(decision_anchor_text):
99
99
  return [
100
100
  _violation(
101
101
  "TDD-RED-001",
@@ -122,7 +122,7 @@ def validate_tdd_evidence(task_dir: Path) -> list[dict]:
122
122
  )
123
123
  ]
124
124
 
125
- acceptance_ids = _acceptance_ids(prd_text)
125
+ acceptance_ids = _acceptance_ids(decision_anchor_text)
126
126
  violations: list[dict] = []
127
127
  for index, entry in enumerate(entries, start=1):
128
128
  if entry.get("type") == "exemption":
@@ -136,8 +136,8 @@ def validate_tdd_evidence(task_dir: Path) -> list[dict]:
136
136
  def validate_tdd_red_evidence(task_dir: Path) -> list[dict]:
137
137
  """Return violations when implementation would start without red evidence."""
138
138
  task_dir = Path(task_dir)
139
- prd_text = _read_text(task_dir / "decision-anchor.md")
140
- if not _task_requires_tdd(prd_text):
139
+ decision_anchor_text = _read_text(task_dir / "decision-anchor.md")
140
+ if not _task_requires_tdd(decision_anchor_text):
141
141
  return []
142
142
 
143
143
  evidence_path = task_dir / EVIDENCE_FILE
@@ -152,7 +152,7 @@ def validate_tdd_red_evidence(task_dir: Path) -> list[dict]:
152
152
  if not red_entries:
153
153
  return [_missing_red_evidence_violation(evidence_path)]
154
154
 
155
- acceptance_ids = _acceptance_ids(prd_text)
155
+ acceptance_ids = _acceptance_ids(decision_anchor_text)
156
156
  violations: list[dict] = []
157
157
  for index, entry in enumerate(entries, start=1):
158
158
  if entry.get("type") == "exemption":
@@ -161,8 +161,8 @@ def validate_tdd_red_evidence(task_dir: Path) -> list[dict]:
161
161
  return violations
162
162
 
163
163
 
164
- def _task_requires_tdd(prd_text: str) -> bool:
165
- lower = prd_text.lower()
164
+ def _task_requires_tdd(decision_anchor_text: str) -> bool:
165
+ lower = decision_anchor_text.lower()
166
166
  if not lower:
167
167
  return False
168
168
  if any(marker.lower() in lower for marker in NON_BEHAVIOR_MARKERS):
@@ -218,10 +218,10 @@ def _read_jsonl(path: Path) -> tuple[list[dict], list[dict]]:
218
218
  return entries, violations
219
219
 
220
220
 
221
- def _acceptance_ids(prd_text: str) -> set[str]:
221
+ def _acceptance_ids(decision_anchor_text: str) -> set[str]:
222
222
  """提取验收标准 ID,支持 AC-001、AC-1、验收标准:1 等格式。"""
223
- ids = set(re.findall(r"\bAC-(\d{1,4})\b", prd_text, re.IGNORECASE))
224
- ids |= set(re.findall(r"验收标准[::]\s*(\d+)", prd_text))
223
+ ids = set(re.findall(r"\bAC-(\d{1,4})\b", decision_anchor_text, re.IGNORECASE))
224
+ ids |= set(re.findall(r"验收标准[::]\s*(\d+)", decision_anchor_text))
225
225
  return {f"AC-{n}" for n in ids}
226
226
 
227
227
 
@@ -285,7 +285,7 @@ def _validate_red_evidence(
285
285
  "TDD-AC-001",
286
286
  f"TDD evidence record {index} references unknown acceptanceId: {acceptance_id}",
287
287
  evidence_path,
288
- "Map evidence to a PRD acceptance ID such as AC-001.",
288
+ "Map evidence to a decision-anchor acceptance criterion such as AC-001.",
289
289
  )
290
290
  )
291
291
 
@@ -347,7 +347,7 @@ def _validate_exemption(
347
347
  "TDD-EXEMPT-002",
348
348
  f"TDD exemption record {index} references unknown acceptanceId: {acceptance_id}",
349
349
  evidence_path,
350
- "Map the exemption to a PRD acceptance ID such as AC-001.",
350
+ "Map the exemption to a decision-anchor acceptance criterion such as AC-001.",
351
351
  )
352
352
  )
353
353
 
@@ -39,6 +39,16 @@ _EXTENSIONS = {
39
39
 
40
40
  _BACKEND_SUFFIXES = re.compile(r"\.(py|java|go|rs|rb|cs|kt|php)$")
41
41
  _FRONTEND_SUFFIXES = re.compile(r"\.(tsx|jsx|vue|svelte|ts|js|mjs|cjs|css|scss)$")
42
+ _SECRET_RE = re.compile(
43
+ r"""(?ix)
44
+ (?:password|passwd|secret|api[_\s]?key|token|access[_\s]?key)
45
+ \s*[:=]\s*["'][^"'}{)($]{3,}["']
46
+ """
47
+ )
48
+ _SILENT_EXCEPT_RE = re.compile(r"^\s*except\b\s*.*:\s*(#.*)?$")
49
+ _BARE_PRINT_RE = re.compile(r"(?<!\w)print\s*\(")
50
+ _CONSOLE_RE = re.compile(r"\bconsole\.(log|info|debug)\s*\(")
51
+ _JS_SUFFIXES = {".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"}
42
52
 
43
53
 
44
54
  def _is_backend_file(rel_path: str) -> bool:
@@ -80,8 +90,8 @@ def load_spec_rules(
80
90
  """Return every natural-language rule declared under ``spec/<category>/``.
81
91
 
82
92
  Re-reads markdown on every call, so user edits take effect without restart.
83
- ``validators`` is preserved as an empty list (legacy field kept so external
84
- tooling that inspects the rule dict does not break).
93
+ The returned rule shape is the current runtime checklist contract:
94
+ category, source, line, and text.
85
95
  """
86
96
  out: list[dict] = []
87
97
  repo_root = Path(repo_root)
@@ -102,7 +112,6 @@ def load_spec_rules(
102
112
  "source": f".cowork-flow/spec/{category}/{md.name}",
103
113
  "line": lineno,
104
114
  "text": rule,
105
- "validators": [],
106
115
  })
107
116
  return out
108
117
 
@@ -171,72 +180,105 @@ def collect_machine_checks(
171
180
  whether to render them in a gate log, inject them into context, or ignore.
172
181
  """
173
182
  violations: list[dict] = []
183
+ for changed_file in _changed_machine_check_files(repo_root):
184
+ violations.extend(_machine_check_violations_for_file(repo_root, changed_file))
185
+ return violations
186
+
187
+
188
+ def _changed_machine_check_files(repo_root: Path) -> Sequence[object]:
174
189
  try:
175
- changed_files = collect_changed_files(repo_root)
190
+ return collect_changed_files(repo_root)
176
191
  except Exception: # noqa: BLE001 — defensively isolate git errors
177
- return violations
192
+ return ()
178
193
 
179
- secret_re = re.compile(
180
- r"""(?ix)
181
- (?:password|passwd|secret|api[_\s]?key|token|access[_\s]?key)
182
- \s*[:=]\s*["'][^"'}{)($]{3,}["']
183
- """
184
- )
185
- silent_except_re = re.compile(r"^\s*except\b\s*.*:\s*(#.*)?$")
186
- bare_print_re = re.compile(r"(?<!\w)print\s*\(")
187
- console_re = re.compile(r"\bconsole\.(log|info|debug)\s*\(")
188
194
 
189
- for changed_file in changed_files:
190
- file_path = repo_root / changed_file.path
191
- if not file_path.is_file():
192
- continue
193
- suffix = file_path.suffix.lower()
194
- if suffix not in _EXTENSIONS:
195
- continue
196
- try:
197
- content = file_path.read_text(encoding="utf-8")
198
- except (OSError, UnicodeDecodeError):
199
- continue
200
- lines = content.splitlines()
201
- for lineno, raw in enumerate(lines, start=1):
202
- # Low-signal heuristic: only flag if the line also has an
203
- # assignment / call so variable-name mentions do not trip it.
204
- if (secret_re.search(raw)
205
- and "process.env" not in raw
206
- and "os.environ" not in raw):
207
- violations.append(_adv(
208
- "MACHINE-HARDCODED-SECRET-001",
209
- "Possible hard-coded secret in source",
210
- changed_file.path, lineno,
211
- "Read the value from configuration or environment variables.",
212
- ))
213
- if suffix == ".py" and bare_print_re.search(raw):
214
- violations.append(_adv(
215
- "MACHINE-DEBUG-PRINT-001",
216
- "Debug print on changed line",
217
- changed_file.path, lineno,
218
- "Use a structured logger or remove the print.",
219
- ))
220
- if suffix in {".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"} and console_re.search(raw):
221
- violations.append(_adv(
222
- "MACHINE-CONSOLE-LOG-001",
223
- "console.{log,info,debug} on changed line",
224
- changed_file.path, lineno,
225
- "Use the project logger or remove the call.",
226
- ))
227
- if (suffix == ".py"
228
- and silent_except_re.match(raw)
229
- and lineno < len(lines)
230
- and lines[lineno].strip() in {"pass", "...", ""}):
231
- violations.append(_adv(
232
- "MACHINE-SILENT-EXCEPT-001",
233
- "Empty exception handler silently swallows errors",
234
- changed_file.path, lineno,
235
- "Log or re-raise the exception, or narrow the except types.",
236
- ))
195
+ def _machine_check_violations_for_file(repo_root: Path, changed_file: object) -> list[dict]:
196
+ file_path = repo_root / changed_file.path
197
+ if not file_path.is_file():
198
+ return []
199
+ suffix = file_path.suffix.lower()
200
+ if suffix not in _EXTENSIONS:
201
+ return []
202
+ try:
203
+ content = file_path.read_text(encoding="utf-8")
204
+ except (OSError, UnicodeDecodeError):
205
+ return []
206
+ return _machine_check_violations_for_lines(changed_file.path, suffix, content.splitlines())
207
+
208
+
209
+ def _machine_check_violations_for_lines(
210
+ rel_path: str,
211
+ suffix: str,
212
+ lines: list[str],
213
+ ) -> list[dict]:
214
+ violations: list[dict] = []
215
+ for lineno, raw in enumerate(lines, start=1):
216
+ violations.extend(_machine_check_violations_for_line(rel_path, suffix, lines, lineno, raw))
217
+ return violations
218
+
219
+
220
+ def _machine_check_violations_for_line(
221
+ rel_path: str,
222
+ suffix: str,
223
+ lines: list[str],
224
+ lineno: int,
225
+ raw: str,
226
+ ) -> list[dict]:
227
+ violations: list[dict] = []
228
+ if _looks_like_hardcoded_secret(raw):
229
+ violations.append(_adv(
230
+ "MACHINE-HARDCODED-SECRET-001",
231
+ "Possible hard-coded secret in source",
232
+ rel_path, lineno,
233
+ "Read the value from configuration or environment variables.",
234
+ ))
235
+ if suffix == ".py" and _BARE_PRINT_RE.search(raw):
236
+ violations.append(_adv(
237
+ "MACHINE-DEBUG-PRINT-001",
238
+ "Debug print on changed line",
239
+ rel_path, lineno,
240
+ "Use a structured logger or remove the print.",
241
+ ))
242
+ if suffix in _JS_SUFFIXES and _CONSOLE_RE.search(raw):
243
+ violations.append(_adv(
244
+ "MACHINE-CONSOLE-LOG-001",
245
+ "console.{log,info,debug} on changed line",
246
+ rel_path, lineno,
247
+ "Use the project logger or remove the call.",
248
+ ))
249
+ if _is_silent_python_except(suffix, lines, lineno, raw):
250
+ violations.append(_adv(
251
+ "MACHINE-SILENT-EXCEPT-001",
252
+ "Empty exception handler silently swallows errors",
253
+ rel_path, lineno,
254
+ "Log or re-raise the exception, or narrow the except types.",
255
+ ))
237
256
  return violations
238
257
 
239
258
 
259
+ def _looks_like_hardcoded_secret(raw: str) -> bool:
260
+ # Low-signal heuristic: only flag if the line also has an assignment / call.
261
+ return (
262
+ _SECRET_RE.search(raw) is not None
263
+ and "process.env" not in raw
264
+ and "os.environ" not in raw
265
+ )
266
+
267
+
268
+ def _is_silent_python_except(
269
+ suffix: str,
270
+ lines: list[str],
271
+ lineno: int,
272
+ raw: str,
273
+ ) -> bool:
274
+ return (
275
+ suffix == ".py"
276
+ and _SILENT_EXCEPT_RE.match(raw) is not None
277
+ and lineno < len(lines)
278
+ and lines[lineno].strip() in {"pass", "...", ""}
279
+ )
280
+
281
+
240
282
  def _adv(
241
283
  rule_id: str,
242
284
  message: str,
@@ -185,20 +185,27 @@ def _check_premature_abstraction(
185
185
  return violations
186
186
 
187
187
 
188
- def _check_unrequested_features(
189
- repo_root: Path,
190
- diff_output: str,
191
- task_dir: Path,
192
- rule_index: dict[str, dict],
193
- ) -> list[dict]:
194
- """R-AG-005: Check if unrequested files were modified (diff files must be in implement.jsonl)."""
195
- violations = []
188
+ def _normalize_allowed_context_file(entry: dict) -> str | None:
189
+ entry_type = entry.get("type", "file")
190
+ file_path = entry.get("file")
191
+ if entry_type not in ("file", "planned-file"):
192
+ return None
193
+ if not isinstance(file_path, str) or not file_path:
194
+ return None
195
+ normalized = file_path.replace("\\", "/")
196
+ while normalized.startswith("./"):
197
+ normalized = normalized[2:]
198
+ segments = normalized.split("/")
199
+ invalid = (
200
+ normalized.startswith("/")
201
+ or re.match(r"^[A-Za-z]:", normalized)
202
+ or any(segment in ("", ".", "..") for segment in segments)
203
+ or any(character in normalized for character in "*?[]")
204
+ )
205
+ return None if invalid else normalized
196
206
 
197
- # Collect allowed files from implement.jsonl
198
- implement_jsonl = task_dir / "implement.jsonl"
199
- if not implement_jsonl.exists():
200
- return violations
201
207
 
208
+ def _load_allowed_context_files(implement_jsonl: Path) -> set[str]:
202
209
  allowed_files: set[str] = set()
203
210
  try:
204
211
  for line in implement_jsonl.read_text(encoding="utf-8").splitlines():
@@ -209,18 +216,28 @@ def _check_unrequested_features(
209
216
  entry = json.loads(line)
210
217
  except json.JSONDecodeError:
211
218
  continue
212
- file_path = entry.get("file")
213
- if isinstance(file_path, str) and file_path:
214
- # Normalize paths (strip repo root prefix if present)
215
- allowed_files.add(file_path)
216
- # Also add stripped versions for matching
217
- if file_path.startswith("./"):
218
- allowed_files.add(file_path[2:])
219
- else:
220
- allowed_files.add(f"./{file_path}")
219
+ normalized = _normalize_allowed_context_file(entry)
220
+ if normalized is not None:
221
+ allowed_files.add(normalized)
221
222
  except (OSError, UnicodeDecodeError):
223
+ return set()
224
+ return allowed_files
225
+
226
+
227
+ def _check_unrequested_features(
228
+ repo_root: Path,
229
+ diff_output: str,
230
+ task_dir: Path,
231
+ rule_index: dict[str, dict],
232
+ ) -> list[dict]:
233
+ """R-AG-005: Require modified files to be explicitly listed."""
234
+ del diff_output
235
+ violations = []
236
+ implement_jsonl = task_dir / "implement.jsonl"
237
+ if not implement_jsonl.exists():
222
238
  return violations
223
239
 
240
+ allowed_files = _load_allowed_context_files(implement_jsonl)
224
241
  if not allowed_files:
225
242
  return violations
226
243
 
@@ -232,7 +249,7 @@ def _check_unrequested_features(
232
249
  continue
233
250
  normalized = file_path.replace("\\", "/")
234
251
  # Check if file is in allowed list
235
- if normalized not in allowed_files and f"./{normalized}" not in allowed_files:
252
+ if normalized not in allowed_files:
236
253
  rule = rule_index.get("R-AG-005")
237
254
  violations.append(
238
255
  rule_violation(