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.
- package/README.md +27 -14
- package/package.json +1 -1
- package/src/commands/init.js +39 -28
- package/src/commands/sync.js +2 -2
- package/src/lib/asset-plan.js +127 -0
- package/src/lib/copy-template.js +101 -141
- package/src/lib/host-assets.js +231 -0
- package/src/lib/plan-applier.js +329 -0
- package/src/lib/platforms.js +15 -76
- package/src/lib/skill-registry.js +503 -0
- package/template/.claude/agents/cowork-check.md +10 -1
- package/template/.claude/agents/cowork-implement.md +11 -2
- package/template/.claude/hooks/inject-workflow-state.py +51 -254
- package/template/.codex/agents/cowork-check.toml +12 -3
- package/template/.codex/agents/cowork-implement.toml +11 -2
- package/template/.codex/agents/default.toml +3 -2
- package/template/.codex/agents/explorer.toml +3 -2
- package/template/.codex/agents/worker.toml +2 -1
- package/template/.codex/hooks/inject-workflow-state.py +46 -265
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/application/__init__.py +1 -0
- package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
- package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
- package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
- package/template/.cowork-flow/scripts/application/task_context.py +765 -0
- package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
- package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
- package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
- package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
- package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
- package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
- package/template/.cowork-flow/scripts/commands/task.py +535 -1953
- package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
- package/template/.cowork-flow/scripts/commands/task_context_commands.py +240 -0
- package/template/.cowork-flow/scripts/commands/task_create_command.py +164 -0
- package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
- package/template/.cowork-flow/scripts/commands/task_parser.py +364 -0
- package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
- package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
- package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
- package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
- package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
- package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
- package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
- package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
- package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
- package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
- package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
- package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
- package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
- package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
- package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
- package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
- package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
- package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
- package/template/.cowork-flow/scripts/common/task/active_task.py +93 -135
- package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
- package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
- package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
- package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
- package/template/.cowork-flow/scripts/run.py +24 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
- package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
- package/template/.cowork-flow/spec/protocols/review.md +34 -0
- package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
- package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
- package/template/.cowork-flow/spec/runtime/index.md +8 -0
- package/template/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
- package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
- package/template/.cowork-flow/workflow.md +18 -13
- package/template/.opencode/agents/cowork-check.md +11 -0
- package/template/.opencode/agents/cowork-implement.md +16 -1
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/inject-context.js +3 -85
- package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
- package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
- package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
- package/template/.zcode/scaffold/AGENTS.md +18 -8
- package/template/AGENTS.md +1 -1
- package/template/skills/batch-mode/SKILL.md +140 -76
- package/template/skills/brainstorming/SKILL.md +2 -2
- package/template/skills/cowork-flow/SKILL.md +42 -0
- package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
- package/template/skills/party-mode/SKILL.md +48 -119
- package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
- package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
- package/template/skills/before-dev/SKILL.md +0 -41
- package/template/skills/check/SKILL.md +0 -53
- package/template/skills/check/simplification-guide.md +0 -47
- package/template/skills/continue/SKILL.md +0 -29
- package/template/skills/doubt-review/SKILL.md +0 -118
- package/template/skills/finish-work/SKILL.md +0 -32
- package/template/skills/party-mode-v2/SKILL.md +0 -75
- package/template/skills/start/SKILL.md +0 -78
- package/template/skills/tdd/SKILL.md +0 -74
- package/template/skills/update-spec/SKILL.md +0 -39
- 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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
140
|
-
if not _task_requires_tdd(
|
|
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(
|
|
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(
|
|
165
|
-
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(
|
|
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",
|
|
224
|
-
ids |= set(re.findall(r"验收标准[::]\s*(\d+)",
|
|
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
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
190
|
+
return collect_changed_files(repo_root)
|
|
176
191
|
except Exception: # noqa: BLE001 — defensively isolate git errors
|
|
177
|
-
return
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
213
|
-
if
|
|
214
|
-
|
|
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
|
|
252
|
+
if normalized not in allowed_files:
|
|
236
253
|
rule = rule_index.get("R-AG-005")
|
|
237
254
|
violations.append(
|
|
238
255
|
rule_violation(
|