pilot-workers 0.5.2__tar.gz → 0.5.3__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.
Files changed (82) hide show
  1. {pilot_workers-0.5.2/src/pilot_workers.egg-info → pilot_workers-0.5.3}/PKG-INFO +1 -1
  2. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/pyproject.toml +1 -1
  3. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/cli/install.py +43 -5
  4. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/integrations/claude-host/skills/pilot-workers/SKILL.md +1 -1
  5. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/integrations/codex-host/skills/pilot-workers/SKILL.md +1 -1
  6. {pilot_workers-0.5.2 → pilot_workers-0.5.3/src/pilot_workers.egg-info}/PKG-INFO +1 -1
  7. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_integrations.py +30 -0
  8. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_skill_generation.py +89 -0
  9. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/LICENSE +0 -0
  10. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/README.md +0 -0
  11. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/setup.cfg +0 -0
  12. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/__init__.py +0 -0
  13. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/__main__.py +0 -0
  14. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/cli/__init__.py +0 -0
  15. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/cli/dispatch.py +0 -0
  16. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/cli/fanout.py +0 -0
  17. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/cli/main.py +0 -0
  18. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/cli/run.py +0 -0
  19. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/cli/status.py +0 -0
  20. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/credentials.py +0 -0
  21. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/permissions/README.md +0 -0
  22. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/permissions/relaxed.yaml +0 -0
  23. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/permissions/strict.yaml +0 -0
  24. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/providers/README.md +0 -0
  25. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/providers/ds.yaml +0 -0
  26. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/providers/glm.yaml +0 -0
  27. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/providers/kimi-k3.yaml +0 -0
  28. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/templates/code.md +0 -0
  29. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/templates/explore.md +0 -0
  30. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/templates/review.md +0 -0
  31. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/data/templates/test.md +0 -0
  32. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/fmt_events.py +0 -0
  33. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/integrations/README.md +0 -0
  34. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/maintain.py +0 -0
  35. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/policy.py +0 -0
  36. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/prompts/code.md +0 -0
  37. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/prompts/common.md +0 -0
  38. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/prompts/explore.md +0 -0
  39. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/prompts/review.md +0 -0
  40. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/prompts/test.md +0 -0
  41. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/providers.py +0 -0
  42. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/runners/__init__.py +0 -0
  43. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/runners/base.py +0 -0
  44. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/runners/opencode_runner.py +0 -0
  45. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/runtime.py +0 -0
  46. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/scripts/install_runtime.sh +0 -0
  47. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers/taskguard.py +0 -0
  48. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers.egg-info/SOURCES.txt +0 -0
  49. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers.egg-info/dependency_links.txt +0 -0
  50. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers.egg-info/entry_points.txt +0 -0
  51. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers.egg-info/requires.txt +0 -0
  52. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/src/pilot_workers.egg-info/top_level.txt +0 -0
  53. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_child_env_hygiene.py +0 -0
  54. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_cli_main.py +0 -0
  55. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_credentials.py +0 -0
  56. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_dispatch.py +0 -0
  57. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_fanout.py +0 -0
  58. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_fanout_watchdog.py +0 -0
  59. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_fmt_events.py +0 -0
  60. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_global_key.py +0 -0
  61. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_install.py +0 -0
  62. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_isolation_guard.py +0 -0
  63. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_lock_takeover.py +0 -0
  64. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_maintain.py +0 -0
  65. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_manifest_locking.py +0 -0
  66. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_opencode_runner.py +0 -0
  67. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_policy.py +0 -0
  68. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_preflight.py +0 -0
  69. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_providers.py +0 -0
  70. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_render_unified.py +0 -0
  71. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_result_extract.py +0 -0
  72. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_run_cli.py +0 -0
  73. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_runner_cache.py +0 -0
  74. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_runner_seams.py +0 -0
  75. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_runtime.py +0 -0
  76. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_sandbox.py +0 -0
  77. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_self_description.py +0 -0
  78. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_status.py +0 -0
  79. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_status_config.py +0 -0
  80. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_task_guards.py +0 -0
  81. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_version_cache.py +0 -0
  82. {pilot_workers-0.5.2 → pilot_workers-0.5.3}/tests/test_worker_prompts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pilot-workers
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Dispatch bounded tasks to isolated LLM workers (GLM, Kimi, and more)
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pilot-workers"
7
- version = "0.5.2"
7
+ version = "0.5.3"
8
8
  description = "Dispatch bounded tasks to isolated LLM workers (GLM, Kimi, and more)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -495,15 +495,52 @@ def apply_generated_region(skill_text: str, region: str) -> str:
495
495
  return before + GENERATED_BEGIN + inner + GENERATED_END + after
496
496
 
497
497
 
498
+ def render_trigger_sentence(provider_keys: list[str],
499
+ modes: dict[str, str]) -> str:
500
+ """The frontmatter's trigger clause, generated from the host's ROUTING.
501
+
502
+ This is the only part of the skill a model sees before deciding whether to
503
+ load it, so it decides what the whole feature can do. It used to read
504
+ "Trigger when the user names a worker provider — ds, kimi-k3", which is the
505
+ most specific clause in the description and therefore the operative one — so
506
+ "explore this codebase" never loaded the skill, and the mode -> provider table
507
+ (which lives in the BODY) was never read. The assignment recorded by
508
+ ``install ds on claude for explore`` was unreachable at trigger time: stored,
509
+ displayed by ``status``, rendered into the skill, and unable to fire.
510
+
511
+ The point of the assignment is that the user does NOT name a provider. So the
512
+ modes come first, in an explicit ``mode, mode -> provider`` form, and naming a
513
+ provider stays as an additional path. A host with no assignments routes
514
+ nothing and must not claim otherwise.
515
+ """
516
+ names = ", ".join(provider_keys)
517
+ routed: dict[str, list[str]] = {}
518
+ for mode, key in sorted(modes.items()):
519
+ if key in provider_keys:
520
+ routed.setdefault(key, []).append(mode)
521
+ if not routed:
522
+ return (f"No mode is routed on this host yet, so nothing is delegated "
523
+ f"automatically; naming a provider ({names}) triggers this skill.")
524
+ groups = "; ".join(
525
+ f"{', '.join(sorted(mode_list))} \u2192 {key}"
526
+ for key, mode_list in sorted(routed.items()))
527
+ return (f"Route by MODE, not by name: {groups}. A request for that kind of "
528
+ f"work triggers this skill even when the user names no provider — "
529
+ f"which is the point of the routing — and naming one ({names}) "
530
+ f"triggers it too.")
531
+
532
+
498
533
  def substitute_trigger_placeholder(skill_text: str,
499
- provider_keys: list[str]) -> str:
500
- """Fill the frontmatter trigger token with the host's provider keys.
534
+ provider_keys: list[str],
535
+ modes: dict[str, str] | None = None) -> str:
536
+ """Fill the frontmatter trigger token.
501
537
 
502
538
  Pure string function: a host's deployed skill names exactly the providers
503
539
  configured for it, so "use <provider>" reaches the skill without leaking
504
540
  the names of providers the host never got.
505
541
  """
506
- return skill_text.replace(TRIGGER_PLACEHOLDER, ", ".join(provider_keys))
542
+ return skill_text.replace(
543
+ TRIGGER_PLACEHOLDER, render_trigger_sentence(provider_keys, modes or {}))
507
544
 
508
545
 
509
546
  def _frontmatter_block(text: str) -> str | None:
@@ -537,7 +574,8 @@ def _render_deployed_skill(installs: dict, host: str, base_text: str) -> str:
537
574
  could never drop a provider that was uninstalled later.
538
575
  """
539
576
  provider_keys = host_providers(installs, host)
540
- region = render_worker_region(provider_keys, host_modes(installs, host))
577
+ modes = host_modes(installs, host)
578
+ region = render_worker_region(provider_keys, modes)
541
579
  try:
542
580
  out = apply_generated_region(base_text, region)
543
581
  except RuntimeError:
@@ -551,7 +589,7 @@ def _render_deployed_skill(installs: dict, host: str, base_text: str) -> str:
551
589
  out = apply_generated_region(_packaged_skill_text(host), region)
552
590
  new_front = _frontmatter_block(
553
591
  substitute_trigger_placeholder(_packaged_skill_text(host),
554
- provider_keys))
592
+ provider_keys, modes))
555
593
  old_front = _frontmatter_block(out)
556
594
  if new_front is not None and old_front is not None:
557
595
  out = new_front + out[len(old_front):]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pilot-workers
3
- description: Dispatch bounded tasks (code, explore, test, review, resume) to isolated LLM workers via the pilot-workers CLI and harvest their structured verdicts. Use whenever a task is worth delegating — bulk mechanical edits, codebase exploration, long test runs, multi-axis code review — so the main session keeps its context for planning and verification. Not for small tweaks, tasks needing mid-course judgment, or anything touching credentials/CI/production. Trigger when the user names a worker provider — {{PILOT_PROVIDER_TRIGGERS}}.
3
+ description: Dispatch bounded tasks (code, explore, test, review, resume) to isolated LLM workers via the pilot-workers CLI and harvest their structured verdicts. Use whenever a task is worth delegating — bulk mechanical edits, codebase exploration, long test runs, multi-axis code review — so the main session keeps its context for planning and verification. Not for small tweaks, tasks needing mid-course judgment, or anything touching credentials/CI/production. {{PILOT_PROVIDER_TRIGGERS}}
4
4
  ---
5
5
 
6
6
  # pilot-workers Playbook
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pilot-workers
3
- description: Plan with Codex, then run bounded tasks (code, explore, test, review, resume) through isolated LLM workers via the pilot-workers CLI and verify their structured verdicts. Invoke explicitly as `$pilot-workers [code|explore|test|review|resume] [task]`. Use whenever the user asks to delegate coding, investigation, testing, review, or session continuation to a worker model {{PILOT_PROVIDER_TRIGGERS}}. Not for small tweaks, mid-course judgment calls, or credentials/CI/production changes.
3
+ description: Plan with Codex, then run bounded tasks (code, explore, test, review, resume) through isolated LLM workers via the pilot-workers CLI and verify their structured verdicts. Invoke explicitly as `$pilot-workers [code|explore|test|review|resume] [task]`. Use whenever coding, investigation, testing, review or session continuation is worth handing to a worker model. {{PILOT_PROVIDER_TRIGGERS}} Not for small tweaks, mid-course judgment calls, or credentials/CI/production changes.
4
4
  ---
5
5
 
6
6
  # pilot-workers Playbook
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pilot-workers
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Dispatch bounded tasks to isolated LLM workers (GLM, Kimi, and more)
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -150,3 +150,33 @@ def test_common_prompt_exists_and_has_no_result_block():
150
150
  assert RESULT_END not in text, (
151
151
  "common.md must not contain the result block (it is per-mode)"
152
152
  )
153
+
154
+
155
+ def test_no_host_template_hardcodes_its_own_trigger_condition():
156
+ """Both templates must hand the whole trigger clause to the placeholder.
157
+
158
+ The clause is generated from the host's routing, so any wording left in the
159
+ template competes with it — and the first pass at this fix patched the claude
160
+ template and missed the codex one, which phrased the same sentence
161
+ differently. Sibling-miss number twelve; the guard costs four lines.
162
+ """
163
+ from pathlib import Path as _Path
164
+
165
+ import pilot_workers
166
+ from pilot_workers.cli import install as install_mod
167
+
168
+ root = _Path(pilot_workers.__file__).resolve().parent / "integrations"
169
+ offenders = []
170
+ for host in ("claude", "codex"):
171
+ path = root / f"{host}-host" / "skills" / "pilot-workers" / "SKILL.md"
172
+ front = install_mod._frontmatter_block(
173
+ path.read_text(encoding="utf-8")) or ""
174
+ normalised = " ".join(front.split())
175
+ assert install_mod.TRIGGER_PLACEHOLDER in normalised, (
176
+ f"{host}: the template lost its trigger placeholder")
177
+ for phrase in ("Trigger when the user names",
178
+ "asks to delegate",
179
+ "to a worker model —"):
180
+ if phrase in normalised:
181
+ offenders.append(f"{host}: still hardcodes {phrase!r}")
182
+ assert not offenders, "; ".join(offenders)
@@ -1177,3 +1177,92 @@ def test_a_file_dropped_from_the_packaged_tree_stops_being_deployed(isolated, ca
1177
1177
  recorded = json.loads(manifest_path.read_text(encoding="utf-8"))
1178
1178
  assert str(stale) not in recorded["installs"]["claude"]["files"]
1179
1179
  assert "removed stale" in capsys.readouterr().out
1180
+
1181
+
1182
+ # ----------------------------------------------------------------------
1183
+ # The trigger must fire on the WORK, not on a provider name.
1184
+ #
1185
+ # The whole point of `install ds on claude for explore` is that the user says
1186
+ # "explore this codebase" and the system picks ds. The generated description
1187
+ # ended with "Trigger when the user names a worker provider — ds, kimi-k3",
1188
+ # which is the most specific clause in it and therefore the operative one, so a
1189
+ # prompt that named no provider never loaded the skill — and the mode -> provider
1190
+ # table lives in the BODY, readable only after loading. The assignment was
1191
+ # unreachable at trigger time: recorded, displayed by `status`, rendered into the
1192
+ # skill, and never able to fire.
1193
+ # ----------------------------------------------------------------------
1194
+
1195
+ def _deployed_description(isolated, providers_and_modes):
1196
+ """Install a host with the given config and return its rendered description."""
1197
+ with install_mod.manifest_transaction() as installs:
1198
+ for key in providers_and_modes["providers"]:
1199
+ install_mod.add_host_provider(installs, "claude", key)
1200
+ for mode, key in providers_and_modes["modes"].items():
1201
+ install_mod.set_host_mode(installs, "claude", mode, key)
1202
+ install_mod.main(["claude", "--target", str(isolated["target"])])
1203
+ text = _claude_skill(isolated).read_text(encoding="utf-8")
1204
+ front = install_mod._frontmatter_block(text)
1205
+ assert front is not None
1206
+ return " ".join(front.split())
1207
+
1208
+
1209
+ def test_the_description_triggers_on_an_assigned_mode(isolated):
1210
+ """A prompt like "explore this codebase" must be able to match.
1211
+
1212
+ Asserted on the exact ROUTING form, not on the mode word: the description's
1213
+ first sentence already lists every mode ("bounded tasks (code, explore,
1214
+ test, review, resume)"), so a substring check for "explore" passes whatever
1215
+ the trigger says. The first version of this test did exactly that and passed
1216
+ against the broken description.
1217
+ """
1218
+ description = _deployed_description(isolated, {
1219
+ "providers": ["ds", "kimi-k3"],
1220
+ "modes": {"explore": "ds", "test": "ds",
1221
+ "code": "kimi-k3", "review": "kimi-k3"},
1222
+ })
1223
+ assert "explore, test → ds" in description, description
1224
+ assert "code, review → kimi-k3" in description, description
1225
+
1226
+
1227
+ def test_the_description_no_longer_makes_a_provider_name_the_condition(isolated):
1228
+ """The narrowing sentence that defeated the whole feature must be gone."""
1229
+ description = _deployed_description(isolated, {
1230
+ "providers": ["ds"], "modes": {"explore": "ds"},
1231
+ })
1232
+ assert "Trigger when the user names a worker provider" not in description, (
1233
+ "the operative clause still requires the user to name a provider")
1234
+
1235
+
1236
+ def test_naming_a_provider_directly_still_triggers(isolated):
1237
+ """The old path must survive: "use ds to ..." is still a valid way in."""
1238
+ description = _deployed_description(isolated, {
1239
+ "providers": ["ds"], "modes": {"explore": "ds"},
1240
+ })
1241
+ assert "ds" in description
1242
+
1243
+
1244
+ def test_a_host_with_no_mode_assignments_does_not_claim_mode_triggering(isolated):
1245
+ """Nothing is routed, so the skill must not advertise that it handles a mode."""
1246
+ description = _deployed_description(isolated, {
1247
+ "providers": ["ds"], "modes": {},
1248
+ })
1249
+ assert "ds" in description
1250
+ for mode in ("explore", "test", "review"):
1251
+ assert f"{mode} →" not in description and f"{mode} ->" not in description, (
1252
+ f"a host with no assignments claims to route {mode}")
1253
+
1254
+
1255
+ def test_an_unassigned_mode_is_not_advertised(isolated):
1256
+ """Only what is actually routed. `code` has no provider here."""
1257
+ description = _deployed_description(isolated, {
1258
+ "providers": ["ds"], "modes": {"explore": "ds"},
1259
+ })
1260
+ routing = install_mod.render_trigger_sentence(["ds"], {"explore": "ds"})
1261
+ assert "explore → ds" in routing
1262
+ # Scoped to the GENERATED clause: the hand-written prose above it legitimately
1263
+ # mentions "bulk mechanical edits" as an example of delegable work, so a check
1264
+ # against the whole description flagged text that was never the problem.
1265
+ for mode in ("code", "review", "test"):
1266
+ assert f"{mode} →" not in routing, (
1267
+ f"the routing clause advertises {mode}, which nothing is assigned to")
1268
+ assert "explore → ds" in description, description
File without changes
File without changes
File without changes