skillflow-py 1.4.0__tar.gz → 1.4.2__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.4.0 → skillflow_py-1.4.2}/PKG-INFO +1 -1
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/pyproject.toml +1 -1
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/core.py +129 -16
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow_py.egg-info/PKG-INFO +1 -1
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_core.py +105 -0
- skillflow_py-1.4.2/tests/test_recovery.py +184 -0
- skillflow_py-1.4.0/tests/test_recovery.py +0 -99
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/LICENSE +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/README.md +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/setup.cfg +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/__init__.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/agent_registry.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/context.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/convert_cli.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/exceptions.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/graph.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/notifications.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/outbox.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/linter/__init__.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/linter/cli.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/linter/tools/skillflow_lint/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/linter/tools/skillflow_lint/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_converter/AGENT.md +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_converter/__init__.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_converter/converter.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_converter/prompts/analyze_skill.md +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_converter/prompts/design_graph.md +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_converter/prompts/explain_design.md +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_converter/prompts/fix_issues.md +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_converter/skill_converter.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_runner/AGENT.md +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_runner/__init__.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/plugins/skill_runner/runner.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/read_tools.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/recovery.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/run_cli.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/schema.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/step_validation.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tool_loader.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/__init__.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/dir_tree/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/dir_tree/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/draft_commit/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/draft_commit/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/file_exists/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/file_exists/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/git_sync_pre/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/git_sync_pre/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/json_schema/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/json_schema/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/lint/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/lint/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/list_tree/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/list_tree/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/notify/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/notify/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/pytest/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/pytest/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/read_file/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/read_file/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/repo_apply/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/repo_apply/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/repo_validate/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/repo_validate/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/write/impl.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/tools/write/tool.yaml +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/validation.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/workspace.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow/write_tools.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow_py.egg-info/SOURCES.txt +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow_py.egg-info/dependency_links.txt +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow_py.egg-info/entry_points.txt +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow_py.egg-info/requires.txt +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/src/skillflow_py.egg-info/top_level.txt +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_context.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_exceptions.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_existing_repo_apply.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_git_sync_pre.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_graph.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_integration.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_integration_configs.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_migration_regression.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_noop_floor_fix.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_notifications.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_outbox.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_read_tools_exec.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_read_tools_label.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_regression.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_step_validation.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_tool_loader.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_tool_lock_contention.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_tools.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_validation.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_workspace.py +0 -0
- {skillflow_py-1.4.0 → skillflow_py-1.4.2}/tests/test_write_tools.py +0 -0
|
@@ -1057,7 +1057,11 @@ class SkillFlow:
|
|
|
1057
1057
|
|
|
1058
1058
|
if not hook_result.get("passed", False):
|
|
1059
1059
|
error = hook_result.get("error", f"Lifecycle hook '{hook_name}' failed")
|
|
1060
|
-
|
|
1060
|
+
# A tool-hook sequence bubbles the failing item's on_failure
|
|
1061
|
+
# in the result; fall back to the spec-level value otherwise.
|
|
1062
|
+
on_failure = hook_result.get("on_failure") or (
|
|
1063
|
+
hook_spec.get("on_failure", "fail")
|
|
1064
|
+
if isinstance(hook_spec, dict) else "fail")
|
|
1061
1065
|
if on_failure == "retry":
|
|
1062
1066
|
self._emit_lifecycle_event(token, hook_name, "retry", error)
|
|
1063
1067
|
self._handle_lifecycle_retry(token, error)
|
|
@@ -1255,19 +1259,64 @@ class SkillFlow:
|
|
|
1255
1259
|
|
|
1256
1260
|
hook_spec can be:
|
|
1257
1261
|
- A dict with 'tool' (single tool call): used for after_validate, on_deliver
|
|
1258
|
-
- A list of
|
|
1262
|
+
- A list of validation specs (multi-check): used for after_deliver
|
|
1263
|
+
- A list of {'tool', 'params'} dicts (sequential tool hooks): used for
|
|
1264
|
+
on_deliver when several repo-mutating tools must run in order (e.g.
|
|
1265
|
+
repo_apply then repo_delete). Each runs via _execute_tool_hook, so
|
|
1266
|
+
$STEP_DIR is resolved and project_root injected — unlike check specs,
|
|
1267
|
+
which run through StepValidator and receive neither.
|
|
1259
1268
|
|
|
1260
1269
|
Returns {passed: bool, error?: str}.
|
|
1261
1270
|
"""
|
|
1262
1271
|
self._emit_lifecycle_event(token, hook_name, "started")
|
|
1263
1272
|
|
|
1264
1273
|
if isinstance(hook_spec, list):
|
|
1265
|
-
|
|
1274
|
+
# after_deliver runs validation checks against the project repo;
|
|
1275
|
+
# other slots (on_deliver) run a sequence of tool hooks with full
|
|
1276
|
+
# variable resolution.
|
|
1277
|
+
if hook_name == "after_deliver":
|
|
1278
|
+
return self._execute_check_hook(token, node, hook_name, hook_spec)
|
|
1279
|
+
return self._execute_tool_hook_sequence(token, node, hook_name, hook_spec)
|
|
1266
1280
|
elif isinstance(hook_spec, dict) and "tool" in hook_spec:
|
|
1267
1281
|
return self._execute_tool_hook(token, node, hook_name, hook_spec)
|
|
1268
1282
|
else:
|
|
1269
1283
|
return {"passed": False, "error": f"Invalid hook spec for '{hook_name}'"}
|
|
1270
1284
|
|
|
1285
|
+
def _execute_tool_hook_sequence(self, token: ClaimToken, node: StepNode,
|
|
1286
|
+
hook_name: str, items: list) -> dict:
|
|
1287
|
+
"""Run a list of tool hooks in order (e.g. repo_apply then repo_delete).
|
|
1288
|
+
|
|
1289
|
+
Each item is a ``{'tool', 'params', 'on_failure'?}`` dict executed via
|
|
1290
|
+
:meth:`_execute_tool_hook` (full variable resolution + project_root
|
|
1291
|
+
injection). Per-item ``on_failure`` is honored: ``warn``/``skip`` log and
|
|
1292
|
+
continue to the next item; ``retry``/``fail`` stop the sequence and
|
|
1293
|
+
bubble up (carrying the item's ``on_failure``) so the caller applies the
|
|
1294
|
+
right step-level outcome.
|
|
1295
|
+
"""
|
|
1296
|
+
detail_files = None
|
|
1297
|
+
for item in items:
|
|
1298
|
+
if not (isinstance(item, dict) and "tool" in item):
|
|
1299
|
+
return {"passed": False,
|
|
1300
|
+
"error": f"Invalid tool hook in '{hook_name}': {item!r}"}
|
|
1301
|
+
res = self._execute_tool_hook(token, node, hook_name, item)
|
|
1302
|
+
if not res.get("passed", True):
|
|
1303
|
+
on_failure = item.get("on_failure", "fail")
|
|
1304
|
+
if on_failure in ("warn", "skip"):
|
|
1305
|
+
self._emit_lifecycle_event(
|
|
1306
|
+
token, hook_name,
|
|
1307
|
+
"warned" if on_failure == "warn" else "skipped",
|
|
1308
|
+
res.get("error", ""))
|
|
1309
|
+
continue
|
|
1310
|
+
# retry / fail: stop and let the caller apply the step outcome.
|
|
1311
|
+
return {**res, "on_failure": on_failure}
|
|
1312
|
+
files = res.get("files")
|
|
1313
|
+
if isinstance(files, list):
|
|
1314
|
+
detail_files = files
|
|
1315
|
+
out = {"passed": True}
|
|
1316
|
+
if detail_files is not None:
|
|
1317
|
+
out["files"] = detail_files
|
|
1318
|
+
return out
|
|
1319
|
+
|
|
1271
1320
|
def _execute_tool_hook(self, token: ClaimToken, node: StepNode,
|
|
1272
1321
|
hook_name: str, hook_spec: dict) -> dict:
|
|
1273
1322
|
"""Execute a tool-type lifecycle hook (single tool call)."""
|
|
@@ -2273,15 +2322,56 @@ class SkillFlow:
|
|
|
2273
2322
|
return inst["id"] if inst else None
|
|
2274
2323
|
|
|
2275
2324
|
def _reopen_tool_step_in_tx(self, run_id: str, step_id: str) -> None:
|
|
2276
|
-
"""Release a claimed tool step back to pending after a
|
|
2277
|
-
so it
|
|
2325
|
+
"""Release a claimed tool step back to pending after a CRASHED execution,
|
|
2326
|
+
so it retries promptly instead of stalling until its claim times out.
|
|
2327
|
+
|
|
2328
|
+
Capped at 3 crashes: a tool that crashes deterministically is broken,
|
|
2329
|
+
not transient, so relaunching it every tick just rampages until the
|
|
2330
|
+
host's step-count valve trips. The SF-20 stale-recovery cap does NOT
|
|
2331
|
+
cover this path (a crash reopens directly, never via
|
|
2332
|
+
recover_stale_claims), so track the count here and fail after 3.
|
|
2278
2333
|
"""
|
|
2279
2334
|
with self._tx() as conn:
|
|
2335
|
+
row = conn.execute(
|
|
2336
|
+
"SELECT id, inputs_json FROM skillflow_steps "
|
|
2337
|
+
"WHERE run_id = ? AND step_id = ? AND status = 'claimed' "
|
|
2338
|
+
"ORDER BY id DESC LIMIT 1",
|
|
2339
|
+
(run_id, step_id),
|
|
2340
|
+
).fetchone()
|
|
2341
|
+
if not row:
|
|
2342
|
+
return
|
|
2343
|
+
inputs = self._deserialize(row["inputs_json"])
|
|
2344
|
+
reopen_count = inputs.get("_tool_reopen_count", 0) + 1
|
|
2345
|
+
if reopen_count >= 3:
|
|
2346
|
+
error_msg = (
|
|
2347
|
+
f"Tool step '{step_id}' crashed {reopen_count} times — "
|
|
2348
|
+
f"failing (likely a bug in the tool, not a transient error)."
|
|
2349
|
+
)
|
|
2350
|
+
conn.execute(
|
|
2351
|
+
"UPDATE skillflow_steps SET status = 'failed', "
|
|
2352
|
+
"version = version + 1, last_error = ?, claimed_at = NULL, "
|
|
2353
|
+
"claimed_by = NULL, updated_at = datetime('now') WHERE id = ?",
|
|
2354
|
+
(error_msg, row["id"]),
|
|
2355
|
+
)
|
|
2356
|
+
conn.execute(
|
|
2357
|
+
"UPDATE skillflow_runs SET current_node = NULL, "
|
|
2358
|
+
"updated_at = datetime('now') WHERE id = ?",
|
|
2359
|
+
(run_id,),
|
|
2360
|
+
)
|
|
2361
|
+
self.notifications.publish_sync(
|
|
2362
|
+
"step_failed",
|
|
2363
|
+
{"run_id": run_id, "step_id": step_id,
|
|
2364
|
+
"error": error_msg, "retryable": False},
|
|
2365
|
+
step_id=step_id, run_id=run_id,
|
|
2366
|
+
)
|
|
2367
|
+
return
|
|
2368
|
+
inputs["_tool_reopen_count"] = reopen_count
|
|
2280
2369
|
conn.execute(
|
|
2281
2370
|
"UPDATE skillflow_steps SET status = 'pending', claimed_at = NULL, "
|
|
2282
|
-
"claimed_by = NULL, version = version + 1,
|
|
2283
|
-
"
|
|
2284
|
-
|
|
2371
|
+
"claimed_by = NULL, version = version + 1, inputs_json = ?, "
|
|
2372
|
+
"updated_at = datetime('now') "
|
|
2373
|
+
"WHERE id = ? AND status = 'claimed'",
|
|
2374
|
+
(self._serialize(inputs), row["id"]),
|
|
2285
2375
|
)
|
|
2286
2376
|
|
|
2287
2377
|
def advance_run(self, run_id: str) -> str | None:
|
|
@@ -2869,18 +2959,41 @@ class SkillFlow:
|
|
|
2869
2959
|
# ── Recovery ──────────────────────────────────────────────────
|
|
2870
2960
|
|
|
2871
2961
|
def recover_stale_claims(self, stale_threshold_seconds: float = 300) -> list[str]:
|
|
2872
|
-
|
|
2873
|
-
"%Y-%m-%dT%H:%M:%SZ",
|
|
2874
|
-
time.gmtime(time.time() - stale_threshold_seconds),
|
|
2875
|
-
)
|
|
2962
|
+
now_epoch = time.time()
|
|
2876
2963
|
with self._tx() as conn:
|
|
2877
|
-
|
|
2964
|
+
claimed = conn.execute(
|
|
2878
2965
|
"""
|
|
2879
|
-
SELECT id, run_id, step_id, inputs_json
|
|
2880
|
-
WHERE status = 'claimed'
|
|
2966
|
+
SELECT id, run_id, step_id, inputs_json, claimed_at
|
|
2967
|
+
FROM skillflow_steps WHERE status = 'claimed'
|
|
2881
2968
|
""",
|
|
2882
|
-
(threshold,),
|
|
2883
2969
|
).fetchall()
|
|
2970
|
+
# For a TOOL step, a claim is stale only once it is older than the
|
|
2971
|
+
# LONGER of the caller's flat threshold and the node's own
|
|
2972
|
+
# timeout_seconds. A slow-but-alive tool (e.g. run_tests, whose node
|
|
2973
|
+
# declares timeout_seconds=1200) must NOT be reclaimed at the flat
|
|
2974
|
+
# threshold — reclaiming it relaunches the tool concurrently with
|
|
2975
|
+
# itself, piling up mutually-starving copies (the step-5 rampage).
|
|
2976
|
+
# Only a claim older than the tool's max legitimate runtime is
|
|
2977
|
+
# presumed dead. Scoped to tool steps on purpose: agent-step
|
|
2978
|
+
# reclaim timing is unchanged (it feeds a separate live
|
|
2979
|
+
# investigation), and this is the only path prone to the rampage
|
|
2980
|
+
# because inline tools re-launch on every advance_run.
|
|
2981
|
+
stale = []
|
|
2982
|
+
for row in claimed:
|
|
2983
|
+
window = stale_threshold_seconds
|
|
2984
|
+
try:
|
|
2985
|
+
node = self._get_resolver_for_run(
|
|
2986
|
+
row["run_id"]).get_node(row["step_id"])
|
|
2987
|
+
if (node and node.step_type == "tool"
|
|
2988
|
+
and node.timeout_seconds
|
|
2989
|
+
and node.timeout_seconds > window):
|
|
2990
|
+
window = float(node.timeout_seconds)
|
|
2991
|
+
except Exception:
|
|
2992
|
+
pass
|
|
2993
|
+
threshold = time.strftime(
|
|
2994
|
+
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(now_epoch - window))
|
|
2995
|
+
if row["claimed_at"] and row["claimed_at"] < threshold:
|
|
2996
|
+
stale.append(row)
|
|
2884
2997
|
run_ids: set[str] = set()
|
|
2885
2998
|
for row in stale:
|
|
2886
2999
|
# SF-20: track stale recovery count to detect crash loops.
|
|
@@ -1142,6 +1142,111 @@ def test_lifecycle_events_in_outbox(sf: SkillFlow, tmp_path: Path):
|
|
|
1142
1142
|
assert step["status"] == "completed"
|
|
1143
1143
|
|
|
1144
1144
|
|
|
1145
|
+
def test_on_deliver_list_runs_tools_end_to_end(sf: SkillFlow, tmp_path: Path):
|
|
1146
|
+
"""on_deliver as a LIST runs its tool hooks (not validation checks): a
|
|
1147
|
+
repo_apply in list form resolves $STEP_DIR and commits into the repo.
|
|
1148
|
+
|
|
1149
|
+
Regression: a list previously routed to StepValidator (check hook), which
|
|
1150
|
+
neither unwraps ``params`` nor resolves ``$STEP_DIR`` — so a repo-mutating
|
|
1151
|
+
tool placed there silently no-op'd.
|
|
1152
|
+
"""
|
|
1153
|
+
import subprocess
|
|
1154
|
+
repo = tmp_path / "repo"
|
|
1155
|
+
repo.mkdir()
|
|
1156
|
+
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
|
1157
|
+
subprocess.run(["git", "config", "user.email", "t@t"], cwd=repo, check=True)
|
|
1158
|
+
subprocess.run(["git", "config", "user.name", "t"], cwd=repo, check=True)
|
|
1159
|
+
|
|
1160
|
+
sf._tool_loader = ToolLoader(
|
|
1161
|
+
Path(__file__).parent.parent / "src" / "skillflow" / "tools")
|
|
1162
|
+
sf._workspace = WorkspaceManager(
|
|
1163
|
+
str(tmp_path / "ws"), projects_base=str(tmp_path / "projects"),
|
|
1164
|
+
code_path_resolver=lambda pid: str(repo),
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
node = StepNode(
|
|
1168
|
+
id="s1", step_type="agent", output_mode="content",
|
|
1169
|
+
output_fixed={"out": "main.py"},
|
|
1170
|
+
lifecycle={"on_deliver": [
|
|
1171
|
+
{"tool": "repo_apply", "params": {"source_dir": "$STEP_DIR"}},
|
|
1172
|
+
]},
|
|
1173
|
+
transitions=[Transition(to=None)],
|
|
1174
|
+
)
|
|
1175
|
+
g = PipelineGraph(name="ondeliver_list", begin="s1", steps=[node])
|
|
1176
|
+
sf.register_graph(g)
|
|
1177
|
+
|
|
1178
|
+
rid = sf.create_run("ondeliver_list", {"project_id": "p1"})
|
|
1179
|
+
sf.start_run(rid)
|
|
1180
|
+
sf.advance_run(rid)
|
|
1181
|
+
token = sf.claim_next_step(rid)
|
|
1182
|
+
tmp = sf._workspace.get_step_tmp_dir("p1", "ondeliver_list", "s1")
|
|
1183
|
+
(tmp / "main.py").write_text("print('hi')\n")
|
|
1184
|
+
sf.confirm_step(token.token, StepResult(outputs={}, flags={}))
|
|
1185
|
+
|
|
1186
|
+
# Landed AND committed → the list ran the tool hook with $STEP_DIR resolved
|
|
1187
|
+
# (old code would have no-op'd via the check-hook path).
|
|
1188
|
+
assert (repo / "main.py").read_text() == "print('hi')\n"
|
|
1189
|
+
log = subprocess.run(["git", "log", "--oneline"], cwd=repo,
|
|
1190
|
+
capture_output=True, text=True).stdout
|
|
1191
|
+
assert "step: s1" in log, f"no apply commit:\n{log}"
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def test_on_deliver_list_order_and_on_failure(sf: SkillFlow, tmp_path: Path,
|
|
1195
|
+
monkeypatch):
|
|
1196
|
+
"""on_deliver list = ordered tool-hook sequence honoring per-item
|
|
1197
|
+
on_failure; after_deliver list still routes to validation checks."""
|
|
1198
|
+
sf._tool_loader = ToolLoader(
|
|
1199
|
+
Path(__file__).parent.parent / "src" / "skillflow" / "tools")
|
|
1200
|
+
sf._workspace = WorkspaceManager(str(tmp_path / "ws"),
|
|
1201
|
+
projects_base=str(tmp_path / "projects"))
|
|
1202
|
+
node = StepNode(
|
|
1203
|
+
id="s1", step_type="agent", output_mode="content",
|
|
1204
|
+
output_fixed={"out": "r.md"}, transitions=[Transition(to=None)],
|
|
1205
|
+
)
|
|
1206
|
+
g = PipelineGraph(name="seq", begin="s1", steps=[node])
|
|
1207
|
+
sf.register_graph(g)
|
|
1208
|
+
rid = sf.create_run("seq", {"project_id": "pid"})
|
|
1209
|
+
sf.start_run(rid)
|
|
1210
|
+
sf.advance_run(rid)
|
|
1211
|
+
token = sf.claim_next_step(rid).token # the ClaimToken inside the ClaimedStep
|
|
1212
|
+
|
|
1213
|
+
calls: list = []
|
|
1214
|
+
|
|
1215
|
+
def fake_tool_hook(tok, nd, hook_name, spec):
|
|
1216
|
+
calls.append(spec["tool"])
|
|
1217
|
+
if spec["tool"] == "boom":
|
|
1218
|
+
return {"passed": False, "error": "kaboom"}
|
|
1219
|
+
return {"passed": True, "files": ["x"]}
|
|
1220
|
+
monkeypatch.setattr(sf, "_execute_tool_hook", fake_tool_hook)
|
|
1221
|
+
|
|
1222
|
+
# 1) ordering + all-pass → sequence passes, surfaces files detail
|
|
1223
|
+
r = sf._execute_lifecycle_hook(token, node, "on_deliver",
|
|
1224
|
+
[{"tool": "repo_apply"}, {"tool": "repo_delete"}])
|
|
1225
|
+
assert calls == ["repo_apply", "repo_delete"]
|
|
1226
|
+
assert r["passed"] is True and r.get("files") == ["x"]
|
|
1227
|
+
|
|
1228
|
+
# 2) failing item with on_failure=warn → continue; sequence still passes
|
|
1229
|
+
calls.clear()
|
|
1230
|
+
r = sf._execute_lifecycle_hook(token, node, "on_deliver",
|
|
1231
|
+
[{"tool": "boom", "on_failure": "warn"}, {"tool": "repo_delete"}])
|
|
1232
|
+
assert calls == ["boom", "repo_delete"]
|
|
1233
|
+
assert r["passed"] is True
|
|
1234
|
+
|
|
1235
|
+
# 3) failing item with default on_failure=fail → stop + bubble on_failure
|
|
1236
|
+
calls.clear()
|
|
1237
|
+
r = sf._execute_lifecycle_hook(token, node, "on_deliver",
|
|
1238
|
+
[{"tool": "boom"}, {"tool": "repo_delete"}])
|
|
1239
|
+
assert calls == ["boom"]
|
|
1240
|
+
assert r["passed"] is False and r["on_failure"] == "fail"
|
|
1241
|
+
|
|
1242
|
+
# 4) after_deliver list is unchanged: routes to checks, not the tool
|
|
1243
|
+
# sequence, so the faked tool-hook path is never touched.
|
|
1244
|
+
calls.clear()
|
|
1245
|
+
sf._execute_lifecycle_hook(token, node, "after_deliver",
|
|
1246
|
+
[{"tool": "syntax_lint", "files": ["*.nomatch"]}])
|
|
1247
|
+
assert calls == []
|
|
1248
|
+
|
|
1249
|
+
|
|
1145
1250
|
# ── Durable run trace ─────────────────────────────────────────────────
|
|
1146
1251
|
|
|
1147
1252
|
def test_trace_append_and_get(sf: SkillFlow):
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Unit tests for recovery.py."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from skillflow.core import SkillFlow
|
|
8
|
+
from skillflow.graph import PipelineGraph, StepNode, Transition
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _agent(id: str, transitions=None):
|
|
12
|
+
return StepNode(id=id, step_type="agent", transitions=transitions or [])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_recover_respects_tool_timeout_seconds(sf: SkillFlow):
|
|
16
|
+
"""A TOOL step claimed longer than the flat threshold but WITHIN its
|
|
17
|
+
timeout_seconds is NOT reclaimed — a slow-but-alive tool (run_tests,
|
|
18
|
+
timeout 1200s) must not be relaunched concurrently with itself (the step-5
|
|
19
|
+
rampage). Only once the claim exceeds the tool timeout is it presumed dead.
|
|
20
|
+
Agent steps are intentionally NOT scoped by this (their reclaim timing
|
|
21
|
+
feeds a separate investigation)."""
|
|
22
|
+
graph = PipelineGraph(
|
|
23
|
+
name="test", begin="a",
|
|
24
|
+
steps=[StepNode(id="a", step_type="tool", tool_name="write",
|
|
25
|
+
timeout_seconds=1200, transitions=[])],
|
|
26
|
+
)
|
|
27
|
+
sf.register_graph(graph)
|
|
28
|
+
run_id = sf.create_run("test")
|
|
29
|
+
sf.start_run(run_id)
|
|
30
|
+
# Seed a claimed tool-step instance claimed 100s ago (already > flat 60s).
|
|
31
|
+
claimed_100s_ago = time.strftime(
|
|
32
|
+
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - 100))
|
|
33
|
+
with sf._lock:
|
|
34
|
+
sf._conn.execute(
|
|
35
|
+
"INSERT INTO skillflow_steps (run_id, step_id, step_config_json, "
|
|
36
|
+
"inputs_json, max_retries, status, claimed_at, created_at, updated_at) "
|
|
37
|
+
"VALUES (?, 'a', '{}', '{}', 3, 'claimed', ?, "
|
|
38
|
+
"datetime('now'), datetime('now'))",
|
|
39
|
+
(run_id, claimed_100s_ago))
|
|
40
|
+
sf._conn.commit()
|
|
41
|
+
|
|
42
|
+
# 100s > flat 60s, but < 1200s tool timeout → presumed ALIVE, NOT reclaimed.
|
|
43
|
+
assert sf.recover_stale_claims(stale_threshold_seconds=60) == []
|
|
44
|
+
st = sf._conn.execute(
|
|
45
|
+
"SELECT status FROM skillflow_steps WHERE run_id=? AND step_id='a' "
|
|
46
|
+
"ORDER BY id DESC LIMIT 1", (run_id,)).fetchone()
|
|
47
|
+
assert st["status"] == "claimed" # still held, not stolen
|
|
48
|
+
|
|
49
|
+
# Backdate beyond the 1200s tool timeout → now presumed dead → reclaimed.
|
|
50
|
+
claimed_1300s_ago = time.strftime(
|
|
51
|
+
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - 1300))
|
|
52
|
+
with sf._lock:
|
|
53
|
+
sf._conn.execute(
|
|
54
|
+
"UPDATE skillflow_steps SET claimed_at=? WHERE run_id=? AND step_id='a'",
|
|
55
|
+
(claimed_1300s_ago, run_id))
|
|
56
|
+
sf._conn.commit()
|
|
57
|
+
assert run_id in sf.recover_stale_claims(stale_threshold_seconds=60)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_tool_reopen_caps_at_three_crashes(sf: SkillFlow):
|
|
61
|
+
"""A tool step that crashes deterministically FAILS after 3 reopens instead
|
|
62
|
+
of being relaunched every tick (which rampaged until the host step-count
|
|
63
|
+
valve). The SF-20 stale-recovery cap doesn't cover the crash-reopen path."""
|
|
64
|
+
graph = PipelineGraph(
|
|
65
|
+
name="test", begin="a",
|
|
66
|
+
steps=[StepNode(id="a", step_type="agent", transitions=[])],
|
|
67
|
+
)
|
|
68
|
+
sf.register_graph(graph)
|
|
69
|
+
run_id = sf.create_run("test")
|
|
70
|
+
sf.start_run(run_id)
|
|
71
|
+
with sf._lock:
|
|
72
|
+
sf._conn.execute(
|
|
73
|
+
"INSERT INTO skillflow_steps (run_id, step_id, step_config_json, "
|
|
74
|
+
"inputs_json, max_retries, status, claimed_at, created_at, updated_at) "
|
|
75
|
+
"VALUES (?, 'a', '{}', '{}', 3, 'claimed', "
|
|
76
|
+
"'2026-01-01T00:00:00Z', datetime('now'), datetime('now'))",
|
|
77
|
+
(run_id,))
|
|
78
|
+
sf._conn.commit()
|
|
79
|
+
|
|
80
|
+
def _status():
|
|
81
|
+
return sf._conn.execute(
|
|
82
|
+
"SELECT status FROM skillflow_steps WHERE run_id=? AND step_id='a' "
|
|
83
|
+
"ORDER BY id DESC LIMIT 1", (run_id,)).fetchone()["status"]
|
|
84
|
+
|
|
85
|
+
# Crashes 1 and 2 reopen to pending; re-claim to simulate the next attempt.
|
|
86
|
+
for _ in range(2):
|
|
87
|
+
sf._reopen_tool_step_in_tx(run_id, "a")
|
|
88
|
+
assert _status() == "pending"
|
|
89
|
+
with sf._lock:
|
|
90
|
+
sf._conn.execute(
|
|
91
|
+
"UPDATE skillflow_steps SET status='claimed' "
|
|
92
|
+
"WHERE run_id=? AND step_id='a'", (run_id,))
|
|
93
|
+
sf._conn.commit()
|
|
94
|
+
|
|
95
|
+
# Third crash → failed, not reopened → no more relaunches.
|
|
96
|
+
sf._reopen_tool_step_in_tx(run_id, "a")
|
|
97
|
+
assert _status() == "failed"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _trans(to: str):
|
|
101
|
+
return Transition(to=to)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_recover_stale_claims_resets_claimed(sf: SkillFlow):
|
|
105
|
+
graph = PipelineGraph(
|
|
106
|
+
name="test", begin="a",
|
|
107
|
+
steps=[_agent("a", [_trans("b")]), _agent("b", [])],
|
|
108
|
+
)
|
|
109
|
+
sf.register_graph(graph)
|
|
110
|
+
run_id = sf.create_run("test")
|
|
111
|
+
sf.start_run(run_id)
|
|
112
|
+
sf.advance_run(run_id)
|
|
113
|
+
sf.claim_next_step(run_id)
|
|
114
|
+
|
|
115
|
+
# Recover with negative threshold (everything is stale)
|
|
116
|
+
recovered = sf.recover_stale_claims(stale_threshold_seconds=-1)
|
|
117
|
+
assert run_id in recovered
|
|
118
|
+
|
|
119
|
+
# Step should be re-claimable
|
|
120
|
+
sf.advance_run(run_id)
|
|
121
|
+
claimed = sf.claim_next_step(run_id)
|
|
122
|
+
assert claimed is not None
|
|
123
|
+
assert claimed.step_id == "a"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_recover_stale_claims_fresh_not_affected(sf: SkillFlow):
|
|
127
|
+
graph = PipelineGraph(
|
|
128
|
+
name="test", begin="a",
|
|
129
|
+
steps=[_agent("a", [])],
|
|
130
|
+
)
|
|
131
|
+
sf.register_graph(graph)
|
|
132
|
+
run_id = sf.create_run("test")
|
|
133
|
+
sf.start_run(run_id)
|
|
134
|
+
sf.advance_run(run_id)
|
|
135
|
+
sf.claim_next_step(run_id)
|
|
136
|
+
|
|
137
|
+
# Default threshold — just-claimed step is fresh
|
|
138
|
+
recovered = sf.recover_stale_claims(stale_threshold_seconds=300)
|
|
139
|
+
assert len(recovered) == 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_recover_stale_claims_no_stale_steps(sf: SkillFlow):
|
|
143
|
+
graph = PipelineGraph(
|
|
144
|
+
name="test", begin="a",
|
|
145
|
+
steps=[_agent("a", [])],
|
|
146
|
+
)
|
|
147
|
+
sf.register_graph(graph)
|
|
148
|
+
sf.create_run("test")
|
|
149
|
+
recovered = sf.recover_stale_claims()
|
|
150
|
+
assert recovered == []
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_recover_stale_keeps_current_node(sf: SkillFlow):
|
|
154
|
+
graph = PipelineGraph(
|
|
155
|
+
name="test", begin="a",
|
|
156
|
+
steps=[_agent("a", [_trans("b")]), _agent("b", [])],
|
|
157
|
+
)
|
|
158
|
+
sf.register_graph(graph)
|
|
159
|
+
run_id = sf.create_run("test")
|
|
160
|
+
sf.start_run(run_id)
|
|
161
|
+
sf.advance_run(run_id)
|
|
162
|
+
sf.claim_next_step(run_id)
|
|
163
|
+
|
|
164
|
+
sf.recover_stale_claims(stale_threshold_seconds=-1)
|
|
165
|
+
run = sf.get_run(run_id)
|
|
166
|
+
# current_node is kept so advance_run re-claims the crashed step
|
|
167
|
+
assert run["current_node"] == "a"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_recover_stale_claims_method_on_instance(sf_tmp: SkillFlow):
|
|
171
|
+
"""Test the SkillFlow.recover_stale_claims() method (replaces standalone function)."""
|
|
172
|
+
|
|
173
|
+
graph = PipelineGraph(
|
|
174
|
+
name="test", begin="a",
|
|
175
|
+
steps=[_agent("a", [])],
|
|
176
|
+
)
|
|
177
|
+
sf_tmp.register_graph(graph)
|
|
178
|
+
run_id = sf_tmp.create_run("test")
|
|
179
|
+
sf_tmp.start_run(run_id)
|
|
180
|
+
sf_tmp.advance_run(run_id)
|
|
181
|
+
sf_tmp.claim_next_step(run_id)
|
|
182
|
+
|
|
183
|
+
recovered = sf_tmp.recover_stale_claims(stale_threshold_seconds=-1)
|
|
184
|
+
assert len(recovered) >= 0
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"""Unit tests for recovery.py."""
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from skillflow.core import SkillFlow
|
|
8
|
-
from skillflow.graph import PipelineGraph, StepNode, Transition
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _agent(id: str, transitions=None):
|
|
12
|
-
return StepNode(id=id, step_type="agent", transitions=transitions or [])
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _trans(to: str):
|
|
16
|
-
return Transition(to=to)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_recover_stale_claims_resets_claimed(sf: SkillFlow):
|
|
20
|
-
graph = PipelineGraph(
|
|
21
|
-
name="test", begin="a",
|
|
22
|
-
steps=[_agent("a", [_trans("b")]), _agent("b", [])],
|
|
23
|
-
)
|
|
24
|
-
sf.register_graph(graph)
|
|
25
|
-
run_id = sf.create_run("test")
|
|
26
|
-
sf.start_run(run_id)
|
|
27
|
-
sf.advance_run(run_id)
|
|
28
|
-
sf.claim_next_step(run_id)
|
|
29
|
-
|
|
30
|
-
# Recover with negative threshold (everything is stale)
|
|
31
|
-
recovered = sf.recover_stale_claims(stale_threshold_seconds=-1)
|
|
32
|
-
assert run_id in recovered
|
|
33
|
-
|
|
34
|
-
# Step should be re-claimable
|
|
35
|
-
sf.advance_run(run_id)
|
|
36
|
-
claimed = sf.claim_next_step(run_id)
|
|
37
|
-
assert claimed is not None
|
|
38
|
-
assert claimed.step_id == "a"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_recover_stale_claims_fresh_not_affected(sf: SkillFlow):
|
|
42
|
-
graph = PipelineGraph(
|
|
43
|
-
name="test", begin="a",
|
|
44
|
-
steps=[_agent("a", [])],
|
|
45
|
-
)
|
|
46
|
-
sf.register_graph(graph)
|
|
47
|
-
run_id = sf.create_run("test")
|
|
48
|
-
sf.start_run(run_id)
|
|
49
|
-
sf.advance_run(run_id)
|
|
50
|
-
sf.claim_next_step(run_id)
|
|
51
|
-
|
|
52
|
-
# Default threshold — just-claimed step is fresh
|
|
53
|
-
recovered = sf.recover_stale_claims(stale_threshold_seconds=300)
|
|
54
|
-
assert len(recovered) == 0
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_recover_stale_claims_no_stale_steps(sf: SkillFlow):
|
|
58
|
-
graph = PipelineGraph(
|
|
59
|
-
name="test", begin="a",
|
|
60
|
-
steps=[_agent("a", [])],
|
|
61
|
-
)
|
|
62
|
-
sf.register_graph(graph)
|
|
63
|
-
sf.create_run("test")
|
|
64
|
-
recovered = sf.recover_stale_claims()
|
|
65
|
-
assert recovered == []
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def test_recover_stale_keeps_current_node(sf: SkillFlow):
|
|
69
|
-
graph = PipelineGraph(
|
|
70
|
-
name="test", begin="a",
|
|
71
|
-
steps=[_agent("a", [_trans("b")]), _agent("b", [])],
|
|
72
|
-
)
|
|
73
|
-
sf.register_graph(graph)
|
|
74
|
-
run_id = sf.create_run("test")
|
|
75
|
-
sf.start_run(run_id)
|
|
76
|
-
sf.advance_run(run_id)
|
|
77
|
-
sf.claim_next_step(run_id)
|
|
78
|
-
|
|
79
|
-
sf.recover_stale_claims(stale_threshold_seconds=-1)
|
|
80
|
-
run = sf.get_run(run_id)
|
|
81
|
-
# current_node is kept so advance_run re-claims the crashed step
|
|
82
|
-
assert run["current_node"] == "a"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_recover_stale_claims_method_on_instance(sf_tmp: SkillFlow):
|
|
86
|
-
"""Test the SkillFlow.recover_stale_claims() method (replaces standalone function)."""
|
|
87
|
-
|
|
88
|
-
graph = PipelineGraph(
|
|
89
|
-
name="test", begin="a",
|
|
90
|
-
steps=[_agent("a", [])],
|
|
91
|
-
)
|
|
92
|
-
sf_tmp.register_graph(graph)
|
|
93
|
-
run_id = sf_tmp.create_run("test")
|
|
94
|
-
sf_tmp.start_run(run_id)
|
|
95
|
-
sf_tmp.advance_run(run_id)
|
|
96
|
-
sf_tmp.claim_next_step(run_id)
|
|
97
|
-
|
|
98
|
-
recovered = sf_tmp.recover_stale_claims(stale_threshold_seconds=-1)
|
|
99
|
-
assert len(recovered) >= 0
|
|
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.4.0 → skillflow_py-1.4.2}/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.4.0 → skillflow_py-1.4.2}/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.4.0 → skillflow_py-1.4.2}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|