skillflow-py 1.3.2__tar.gz → 1.4.0__tar.gz
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.
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/PKG-INFO +1 -1
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/pyproject.toml +1 -1
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/read_tools.py +31 -8
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/repo_apply/impl.py +17 -26
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/repo_apply/tool.yaml +4 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow_py.egg-info/PKG-INFO +1 -1
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow_py.egg-info/SOURCES.txt +1 -0
- skillflow_py-1.4.0/tests/test_noop_floor_fix.py +65 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_tools.py +61 -1
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/LICENSE +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/README.md +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/setup.cfg +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/__init__.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/agent_registry.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/context.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/convert_cli.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/core.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/exceptions.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/graph.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/notifications.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/outbox.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/linter/__init__.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/linter/cli.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/linter/tools/skillflow_lint/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/linter/tools/skillflow_lint/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/AGENT.md +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/__init__.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/converter.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/prompts/analyze_skill.md +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/prompts/design_graph.md +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/prompts/explain_design.md +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/prompts/fix_issues.md +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/skill_converter.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_runner/AGENT.md +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_runner/__init__.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_runner/runner.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/recovery.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/run_cli.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/schema.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/step_validation.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tool_loader.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/__init__.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/dir_tree/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/dir_tree/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/draft_commit/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/draft_commit/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/file_exists/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/file_exists/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/git_sync_pre/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/git_sync_pre/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/json_schema/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/json_schema/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/lint/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/lint/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/list_tree/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/list_tree/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/notify/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/notify/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/pytest/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/pytest/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/read_file/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/read_file/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/repo_validate/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/repo_validate/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/write/impl.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/tools/write/tool.yaml +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/validation.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/workspace.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/write_tools.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow_py.egg-info/dependency_links.txt +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow_py.egg-info/entry_points.txt +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow_py.egg-info/requires.txt +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow_py.egg-info/top_level.txt +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_context.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_core.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_exceptions.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_existing_repo_apply.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_git_sync_pre.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_graph.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_integration.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_integration_configs.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_migration_regression.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_notifications.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_outbox.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_read_tools_exec.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_read_tools_label.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_recovery.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_regression.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_step_validation.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_tool_loader.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_tool_lock_contention.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_validation.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_workspace.py +0 -0
- {skillflow_py-1.3.2 → skillflow_py-1.4.0}/tests/test_write_tools.py +0 -0
|
@@ -27,6 +27,21 @@ from pathlib import Path
|
|
|
27
27
|
# the ``truncated`` flag signals when there is more to page through.
|
|
28
28
|
_MAX_READ_LINES = 2000
|
|
29
29
|
|
|
30
|
+
# Directories never worth enumerating/searching as "source" — VCS internals and
|
|
31
|
+
# build/dependency caches. Mirrors dir_tree/list_tree's BLOCKED set, which the
|
|
32
|
+
# generated directory read tools previously lacked: a single list_repo_root on a
|
|
33
|
+
# real repo dumped the whole .git tree (~64% of files) into the agent's context.
|
|
34
|
+
_BLOCKED_DIR_PARTS = {".git", "__pycache__", ".venv", "node_modules",
|
|
35
|
+
".mypy_cache", ".pytest_cache", ".ruff_cache"}
|
|
36
|
+
# Hard cap on entries returned by a directory listing so one call can't flood
|
|
37
|
+
# the context with thousands of paths.
|
|
38
|
+
_MAX_LIST_ENTRIES = 1000
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_blocked_path(rel) -> bool:
|
|
42
|
+
"""True if any path component is a blocked VCS/build directory."""
|
|
43
|
+
return any(part in _BLOCKED_DIR_PARTS for part in Path(rel).parts)
|
|
44
|
+
|
|
30
45
|
|
|
31
46
|
def _page_lines(text: str, start_line: int = 0, end_line: int | None = None) -> dict:
|
|
32
47
|
"""Slice file text into a line-numbered window with paging metadata.
|
|
@@ -407,19 +422,25 @@ def make_read_tool_fns(specs: list[dict], workspace_root: str,
|
|
|
407
422
|
|
|
408
423
|
def _list_dir(_paths=dir_paths) -> str:
|
|
409
424
|
entries = []
|
|
425
|
+
truncated = False
|
|
410
426
|
for dp in _paths:
|
|
411
427
|
d = Path(dp)
|
|
412
428
|
if not d.is_dir():
|
|
413
429
|
continue
|
|
414
430
|
for f in sorted(d.rglob("*")):
|
|
415
|
-
if f.is_file() and f.name != ".gitkeep":
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
431
|
+
if not (f.is_file() and f.name != ".gitkeep"):
|
|
432
|
+
continue
|
|
433
|
+
rel = f.relative_to(d)
|
|
434
|
+
if _is_blocked_path(rel):
|
|
435
|
+
continue # skip .git / build / dependency caches
|
|
436
|
+
entries.append({"name": str(rel), "size": f.stat().st_size})
|
|
437
|
+
if len(entries) >= _MAX_LIST_ENTRIES:
|
|
438
|
+
truncated = True
|
|
439
|
+
break
|
|
440
|
+
if truncated:
|
|
441
|
+
break
|
|
442
|
+
return json.dumps({"files": entries, "truncated": truncated},
|
|
443
|
+
ensure_ascii=False)
|
|
423
444
|
|
|
424
445
|
def _read_dir_file(name: str, start_line: int = 0,
|
|
425
446
|
end_line: int | None = None, _paths=dir_paths) -> dict:
|
|
@@ -465,6 +486,8 @@ def make_read_tool_fns(specs: list[dict], workspace_root: str,
|
|
|
465
486
|
for f in sorted(d.rglob(glob) if glob else d.rglob("*")):
|
|
466
487
|
if not f.is_file() or f.name == ".gitkeep":
|
|
467
488
|
continue
|
|
489
|
+
if _is_blocked_path(f.relative_to(d)):
|
|
490
|
+
continue # skip .git / build / dependency caches
|
|
468
491
|
# Skip binary-looking files
|
|
469
492
|
if f.suffix in (".pyc", ".pyo", ".so", ".o", ".bin"):
|
|
470
493
|
continue
|
|
@@ -1,39 +1,31 @@
|
|
|
1
1
|
"""Apply files from draft directory to project repo."""
|
|
2
2
|
|
|
3
|
+
import fnmatch
|
|
3
4
|
import shutil
|
|
4
5
|
import subprocess
|
|
5
|
-
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def _probe(msg: str) -> None:
|
|
10
|
-
"""Diagnostic log for the step-dir-contains-whole-repo investigation.
|
|
11
|
-
|
|
12
|
-
Prints to stderr with a greppable marker so it shows in `docker logs`.
|
|
13
|
-
"""
|
|
14
|
-
print(f"[repo_apply.probe] {msg}", file=sys.stderr, flush=True)
|
|
15
|
-
|
|
16
|
-
|
|
17
9
|
def repo_apply(source_dir: str, *, workspace_root: str = "",
|
|
18
10
|
project_root: str = "",
|
|
19
11
|
step_id: str = "", project_id: str = "",
|
|
20
|
-
task_name: str = "") -> dict:
|
|
12
|
+
task_name: str = "", ignore=None) -> dict:
|
|
21
13
|
src = Path(source_dir)
|
|
22
14
|
if not src.is_absolute():
|
|
23
15
|
src = Path(workspace_root) / source_dir
|
|
24
16
|
src = src.resolve()
|
|
25
17
|
dst = Path(project_root).resolve()
|
|
26
18
|
|
|
27
|
-
# PROBE: what is repo_apply actually reading from, and is it the repo itself?
|
|
28
|
-
_probe(f"step={step_id} project={project_id} task={task_name!r} "
|
|
29
|
-
f"source_dir_param={source_dir!r} resolved_src={src} "
|
|
30
|
-
f"project_root={dst} workspace_root={workspace_root!r} "
|
|
31
|
-
f"src==dst? {src == dst}")
|
|
32
|
-
|
|
33
19
|
if not src.exists():
|
|
34
20
|
return {"applied": False, "files": [],
|
|
35
21
|
"error": f"Source dir not found: {source_dir}"}
|
|
36
22
|
|
|
23
|
+
# Caller-supplied ignore globs (fnmatch), matched against BOTH the path
|
|
24
|
+
# relative to source_dir AND the basename — so "web/js/_x.json" and "_x.json"
|
|
25
|
+
# style patterns both work. Lets a host keep control/scratch files (e.g. a
|
|
26
|
+
# deletions manifest) in the step dir without them landing in the repo.
|
|
27
|
+
ignore_globs = tuple(ignore or ())
|
|
28
|
+
|
|
37
29
|
applied_files = []
|
|
38
30
|
for f in sorted(src.rglob("*")):
|
|
39
31
|
if not f.is_file():
|
|
@@ -43,22 +35,21 @@ def repo_apply(source_dir: str, *, workspace_root: str = "",
|
|
|
43
35
|
if ".git/" in str(f):
|
|
44
36
|
continue
|
|
45
37
|
rel = f.relative_to(src)
|
|
38
|
+
if ignore_globs and any(
|
|
39
|
+
fnmatch.fnmatch(str(rel), pat) or fnmatch.fnmatch(f.name, pat)
|
|
40
|
+
for pat in ignore_globs
|
|
41
|
+
):
|
|
42
|
+
continue
|
|
46
43
|
target = dst / rel
|
|
47
44
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
48
45
|
shutil.copy2(f, target)
|
|
49
46
|
applied_files.append(str(rel))
|
|
50
47
|
|
|
51
|
-
# PROBE: how many files did we just apply? A handful = normal surgical edit;
|
|
52
|
-
# dozens/hundreds = the leak. Dump a sample so we can see WHAT leaked.
|
|
53
|
-
if len(applied_files) > 15:
|
|
54
|
-
_probe(f"LARGE APPLY step={step_id}: {len(applied_files)} files from "
|
|
55
|
-
f"{src} — first 15: {applied_files[:15]}")
|
|
56
|
-
else:
|
|
57
|
-
_probe(f"step={step_id}: applied {len(applied_files)} file(s): {applied_files}")
|
|
58
|
-
|
|
59
48
|
if not applied_files:
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
# Empty source = a legitimate no-op step (the agent determined no change
|
|
50
|
+
# was needed and produced no output). Nothing to copy or commit; report
|
|
51
|
+
# success so the on_deliver hook doesn't retry/fail a clean no-op.
|
|
52
|
+
return {"applied": True, "files": [], "committed": False}
|
|
62
53
|
|
|
63
54
|
# git add + commit
|
|
64
55
|
r = subprocess.run(["git", "add", "-A"], cwd=dst,
|
|
@@ -8,3 +8,7 @@ parameters:
|
|
|
8
8
|
type: string
|
|
9
9
|
description: "Source directory path (absolute or relative to workspace)"
|
|
10
10
|
required: true
|
|
11
|
+
ignore:
|
|
12
|
+
type: array
|
|
13
|
+
description: "Optional list of fnmatch globs (matched against the path relative to source_dir and the basename). Matching files are NOT copied to the repo — use for host control/scratch files kept in the step dir (e.g. a deletions manifest)."
|
|
14
|
+
required: false
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Regression tests for the existing-repo "no-op floor" data-corruption bug.
|
|
2
|
+
|
|
3
|
+
Two skillflow-side guarantees the host (AItelier) relies on after the fix:
|
|
4
|
+
|
|
5
|
+
1. ``repo_apply`` on an EMPTY source dir is a no-op SUCCESS, not an error.
|
|
6
|
+
A legitimate "no change needed" step promotes empty staging; on_deliver
|
|
7
|
+
must not retry/fail it.
|
|
8
|
+
|
|
9
|
+
2. The generated directory read tools (``list_<label>`` / ``search_<label>``)
|
|
10
|
+
exclude ``.git`` and build/dependency caches and cap their output, so a
|
|
11
|
+
single ``list_repo_root`` can't dump the whole ``.git`` tree (tens of
|
|
12
|
+
thousands of tokens) into the agent's context.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from skillflow.read_tools import _is_blocked_path, make_read_tool_fns
|
|
20
|
+
from skillflow.tool_loader import ToolLoader
|
|
21
|
+
|
|
22
|
+
_REAL_TOOLS = Path(__file__).parent.parent / "src" / "skillflow" / "tools"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_repo_apply_empty_source_is_noop_success(tmp_path):
|
|
26
|
+
repo = tmp_path / "repo"
|
|
27
|
+
repo.mkdir()
|
|
28
|
+
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
|
29
|
+
|
|
30
|
+
src = tmp_path / "empty_staging"
|
|
31
|
+
src.mkdir() # no files
|
|
32
|
+
|
|
33
|
+
repo_apply = ToolLoader(_REAL_TOOLS).load_fn("repo_apply")
|
|
34
|
+
res = repo_apply(str(src), project_root=str(repo))
|
|
35
|
+
|
|
36
|
+
assert res["applied"] is True
|
|
37
|
+
assert res["files"] == []
|
|
38
|
+
assert res.get("committed") is False
|
|
39
|
+
assert "error" not in res
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_is_blocked_path_excludes_vcs_and_build_dirs():
|
|
43
|
+
assert _is_blocked_path(Path(".git/objects/ab/cd"))
|
|
44
|
+
assert _is_blocked_path("node_modules/foo/bar.js")
|
|
45
|
+
assert _is_blocked_path("pkg/__pycache__/x.pyc")
|
|
46
|
+
assert not _is_blocked_path("src/main.py")
|
|
47
|
+
assert not _is_blocked_path("docs/readme.md")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_list_repo_tool_excludes_git_tree(tmp_path):
|
|
51
|
+
repo = tmp_path / "repo"
|
|
52
|
+
(repo / ".git" / "objects").mkdir(parents=True)
|
|
53
|
+
(repo / ".git" / "objects" / "deadbeef").write_text("binblob")
|
|
54
|
+
(repo / "src").mkdir()
|
|
55
|
+
(repo / "src" / "main.py").write_text("x = 1\n")
|
|
56
|
+
|
|
57
|
+
specs = [{"source_type": "repository", "mode": "tool"}]
|
|
58
|
+
fns = make_read_tool_fns(specs, str(tmp_path), code_root=str(repo))
|
|
59
|
+
|
|
60
|
+
list_fn = next(fn for name, fn in fns.items() if name.startswith("list_"))
|
|
61
|
+
payload = json.loads(list_fn())
|
|
62
|
+
names = [e["name"] for e in payload["files"]]
|
|
63
|
+
|
|
64
|
+
assert "src/main.py" in names
|
|
65
|
+
assert not any(".git" in n for n in names), names
|
|
@@ -144,7 +144,67 @@ class TestRepoApply:
|
|
|
144
144
|
result = repo_apply(source_dir=str(empty),
|
|
145
145
|
workspace_root=str(tmp_path),
|
|
146
146
|
project_root=str(proj))
|
|
147
|
-
|
|
147
|
+
# Empty source is a legitimate no-op step (agent determined no change
|
|
148
|
+
# needed): report success with no files, not an error that would make
|
|
149
|
+
# the on_deliver hook retry/fail a clean no-op.
|
|
150
|
+
assert result["applied"] is True
|
|
151
|
+
assert result["files"] == []
|
|
152
|
+
assert result.get("committed") is False
|
|
153
|
+
|
|
154
|
+
def test_ignore_globs_not_copied(self, tmp_path):
|
|
155
|
+
"""Files matching an `ignore` glob stay out of the repo (host control
|
|
156
|
+
files kept in the step dir, e.g. a deletions manifest)."""
|
|
157
|
+
proj = tmp_path / "project"
|
|
158
|
+
proj.mkdir()
|
|
159
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=proj,
|
|
160
|
+
capture_output=True)
|
|
161
|
+
subprocess.run(["git", "config", "user.email", "test@test.com"],
|
|
162
|
+
cwd=proj, capture_output=True)
|
|
163
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=proj,
|
|
164
|
+
capture_output=True)
|
|
165
|
+
(proj / ".gitkeep").write_text("")
|
|
166
|
+
subprocess.run(["git", "add", "."], cwd=proj, capture_output=True)
|
|
167
|
+
subprocess.run(["git", "commit", "-m", "init"], cwd=proj,
|
|
168
|
+
capture_output=True)
|
|
169
|
+
|
|
170
|
+
draft = tmp_path / "draft"
|
|
171
|
+
draft.mkdir()
|
|
172
|
+
(draft / "keep.py").write_text("x = 1\n")
|
|
173
|
+
(draft / "_deletions.json").write_text('["old.js"]')
|
|
174
|
+
(draft / "sub").mkdir()
|
|
175
|
+
(draft / "sub" / "_deletions.json").write_text('["y"]')
|
|
176
|
+
|
|
177
|
+
result = repo_apply(source_dir=str(draft),
|
|
178
|
+
workspace_root=str(tmp_path),
|
|
179
|
+
project_root=str(proj),
|
|
180
|
+
ignore=["_deletions.json"])
|
|
181
|
+
|
|
182
|
+
assert result["applied"] is True
|
|
183
|
+
assert "keep.py" in result["files"]
|
|
184
|
+
# basename-matched at any depth → never copied, never committed
|
|
185
|
+
assert "_deletions.json" not in result["files"]
|
|
186
|
+
assert "sub/_deletions.json" not in result["files"]
|
|
187
|
+
assert (proj / "keep.py").exists()
|
|
188
|
+
assert not (proj / "_deletions.json").exists()
|
|
189
|
+
assert not (proj / "sub" / "_deletions.json").exists()
|
|
190
|
+
|
|
191
|
+
def test_ignore_default_none_copies_everything(self, tmp_path):
|
|
192
|
+
"""Backward-compat: no `ignore` arg → every file copied (unchanged)."""
|
|
193
|
+
proj = tmp_path / "project"
|
|
194
|
+
proj.mkdir()
|
|
195
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=proj,
|
|
196
|
+
capture_output=True)
|
|
197
|
+
subprocess.run(["git", "config", "user.email", "t@t"], cwd=proj,
|
|
198
|
+
capture_output=True)
|
|
199
|
+
subprocess.run(["git", "config", "user.name", "T"], cwd=proj,
|
|
200
|
+
capture_output=True)
|
|
201
|
+
draft = tmp_path / "draft"
|
|
202
|
+
draft.mkdir()
|
|
203
|
+
(draft / "_deletions.json").write_text("[]")
|
|
204
|
+
result = repo_apply(source_dir=str(draft),
|
|
205
|
+
workspace_root=str(tmp_path),
|
|
206
|
+
project_root=str(proj))
|
|
207
|
+
assert "_deletions.json" in result["files"]
|
|
148
208
|
|
|
149
209
|
|
|
150
210
|
class TestLint:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/linter/tools/skillflow_lint/impl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/converter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skillflow_py-1.3.2 → skillflow_py-1.4.0}/src/skillflow/plugins/skill_converter/skill_converter.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|