pilot-workers 0.5.2__tar.gz → 0.5.4__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 (95) hide show
  1. {pilot_workers-0.5.2/src/pilot_workers.egg-info → pilot_workers-0.5.4}/PKG-INFO +3 -3
  2. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/README.md +2 -2
  3. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/pyproject.toml +1 -1
  4. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/cli/install.py +123 -68
  5. pilot_workers-0.5.4/src/pilot_workers/integrations/claude-host/skills/pilot-workers/SKILL.md +148 -0
  6. pilot_workers-0.5.4/src/pilot_workers/integrations/claude-host/skills/pilot-workers/modes/code.md +41 -0
  7. pilot_workers-0.5.4/src/pilot_workers/integrations/claude-host/skills/pilot-workers/modes/explore.md +25 -0
  8. pilot_workers-0.5.4/src/pilot_workers/integrations/claude-host/skills/pilot-workers/modes/resume.md +27 -0
  9. pilot_workers-0.5.4/src/pilot_workers/integrations/claude-host/skills/pilot-workers/modes/review.md +37 -0
  10. pilot_workers-0.5.4/src/pilot_workers/integrations/claude-host/skills/pilot-workers/modes/test.md +15 -0
  11. pilot_workers-0.5.4/src/pilot_workers/integrations/codex-host/skills/pilot-workers/SKILL.md +131 -0
  12. pilot_workers-0.5.4/src/pilot_workers/integrations/codex-host/skills/pilot-workers/modes/code.md +38 -0
  13. pilot_workers-0.5.4/src/pilot_workers/integrations/codex-host/skills/pilot-workers/modes/explore.md +22 -0
  14. pilot_workers-0.5.4/src/pilot_workers/integrations/codex-host/skills/pilot-workers/modes/resume.md +25 -0
  15. pilot_workers-0.5.4/src/pilot_workers/integrations/codex-host/skills/pilot-workers/modes/review.md +36 -0
  16. pilot_workers-0.5.4/src/pilot_workers/integrations/codex-host/skills/pilot-workers/modes/test.md +14 -0
  17. {pilot_workers-0.5.2 → pilot_workers-0.5.4/src/pilot_workers.egg-info}/PKG-INFO +3 -3
  18. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers.egg-info/SOURCES.txt +10 -0
  19. pilot_workers-0.5.4/tests/test_integrations.py +248 -0
  20. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_skill_generation.py +313 -57
  21. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_worker_prompts.py +1 -1
  22. pilot_workers-0.5.2/src/pilot_workers/integrations/claude-host/skills/pilot-workers/SKILL.md +0 -256
  23. pilot_workers-0.5.2/src/pilot_workers/integrations/codex-host/skills/pilot-workers/SKILL.md +0 -231
  24. pilot_workers-0.5.2/tests/test_integrations.py +0 -152
  25. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/LICENSE +0 -0
  26. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/setup.cfg +0 -0
  27. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/__init__.py +0 -0
  28. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/__main__.py +0 -0
  29. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/cli/__init__.py +0 -0
  30. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/cli/dispatch.py +0 -0
  31. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/cli/fanout.py +0 -0
  32. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/cli/main.py +0 -0
  33. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/cli/run.py +0 -0
  34. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/cli/status.py +0 -0
  35. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/credentials.py +0 -0
  36. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/permissions/README.md +0 -0
  37. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/permissions/relaxed.yaml +0 -0
  38. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/permissions/strict.yaml +0 -0
  39. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/providers/README.md +0 -0
  40. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/providers/ds.yaml +0 -0
  41. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/providers/glm.yaml +0 -0
  42. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/providers/kimi-k3.yaml +0 -0
  43. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/templates/code.md +0 -0
  44. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/templates/explore.md +0 -0
  45. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/templates/review.md +0 -0
  46. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/data/templates/test.md +0 -0
  47. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/fmt_events.py +0 -0
  48. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/integrations/README.md +0 -0
  49. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/maintain.py +0 -0
  50. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/policy.py +0 -0
  51. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/prompts/code.md +0 -0
  52. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/prompts/common.md +0 -0
  53. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/prompts/explore.md +0 -0
  54. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/prompts/review.md +0 -0
  55. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/prompts/test.md +0 -0
  56. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/providers.py +0 -0
  57. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/runners/__init__.py +0 -0
  58. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/runners/base.py +0 -0
  59. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/runners/opencode_runner.py +0 -0
  60. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/runtime.py +0 -0
  61. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/scripts/install_runtime.sh +0 -0
  62. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers/taskguard.py +0 -0
  63. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers.egg-info/dependency_links.txt +0 -0
  64. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers.egg-info/entry_points.txt +0 -0
  65. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers.egg-info/requires.txt +0 -0
  66. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/src/pilot_workers.egg-info/top_level.txt +0 -0
  67. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_child_env_hygiene.py +0 -0
  68. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_cli_main.py +0 -0
  69. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_credentials.py +0 -0
  70. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_dispatch.py +0 -0
  71. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_fanout.py +0 -0
  72. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_fanout_watchdog.py +0 -0
  73. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_fmt_events.py +0 -0
  74. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_global_key.py +0 -0
  75. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_install.py +0 -0
  76. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_isolation_guard.py +0 -0
  77. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_lock_takeover.py +0 -0
  78. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_maintain.py +0 -0
  79. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_manifest_locking.py +0 -0
  80. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_opencode_runner.py +0 -0
  81. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_policy.py +0 -0
  82. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_preflight.py +0 -0
  83. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_providers.py +0 -0
  84. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_render_unified.py +0 -0
  85. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_result_extract.py +0 -0
  86. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_run_cli.py +0 -0
  87. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_runner_cache.py +0 -0
  88. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_runner_seams.py +0 -0
  89. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_runtime.py +0 -0
  90. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_sandbox.py +0 -0
  91. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_self_description.py +0 -0
  92. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_status.py +0 -0
  93. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_status_config.py +0 -0
  94. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_task_guards.py +0 -0
  95. {pilot_workers-0.5.2 → pilot_workers-0.5.4}/tests/test_version_cache.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.4
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
@@ -119,12 +119,12 @@ Reserved keys (cannot be used as provider key): `runner`, `all`, `on`, `for`, `k
119
119
 
120
120
  ## Host integration
121
121
 
122
- The **host** is whatever AI agent acts as the planner. Each host ships ONE `pilot-workers` playbook skill (a doctrine playbook, not provider-specific syntax):
122
+ The **host** is whatever AI agent acts as the planner. Each host ships ONE `pilot-workers` playbook skill (a doctrine playbook, not provider-specific syntax) — a core `SKILL.md` plus five on-demand `modes/<mode>.md` playbooks, so triggering loads the core only and mode craft is read per dispatch:
123
123
 
124
124
  - **`claude-host/skills/pilot-workers/`**: playbook skill for Claude Code (installed to `~/.claude/skills/pilot-workers/`)
125
125
  - **`codex-host/skills/pilot-workers/`**: the same playbook skill for Codex (installed to `$CODEX_HOME/skills/pilot-workers/`)
126
126
 
127
- `pilot-workers install <provider> on <host>` copies the skill into the host's skill directory and regenerates its worker table from that host's configuration; the doctrine itself carries no engine-specific knowledge. Removing the last provider deletes the skill, so a host with nothing configured has none and the planner does the work itself. (The v0.4.0 12-agents + 8-commands matrix is gone.)
127
+ `pilot-workers install <provider> on <host>` copies the skill into the host's skill directory and regenerates its worker table from that host's configuration; the doctrine itself carries no engine-specific knowledge. The deployed tree is a build artifact — every install re-renders it from the packaged template, so upgrading is `pip` upgrade + `pilot-workers install <host|all>` (hosts with nothing configured are untouched). Removing the last provider deletes the skill, so a host with nothing configured has none and the planner does the work itself. (The v0.4.0 12-agents + 8-commands matrix is gone.)
128
128
 
129
129
  Adding a new host: create `integrations/<name>-host/skills/pilot-workers/`, put whatever config your host needs, point it at `pilot-workers dispatch`. See `integrations/README.md`.
130
130
 
@@ -104,12 +104,12 @@ Reserved keys (cannot be used as provider key): `runner`, `all`, `on`, `for`, `k
104
104
 
105
105
  ## Host integration
106
106
 
107
- The **host** is whatever AI agent acts as the planner. Each host ships ONE `pilot-workers` playbook skill (a doctrine playbook, not provider-specific syntax):
107
+ The **host** is whatever AI agent acts as the planner. Each host ships ONE `pilot-workers` playbook skill (a doctrine playbook, not provider-specific syntax) — a core `SKILL.md` plus five on-demand `modes/<mode>.md` playbooks, so triggering loads the core only and mode craft is read per dispatch:
108
108
 
109
109
  - **`claude-host/skills/pilot-workers/`**: playbook skill for Claude Code (installed to `~/.claude/skills/pilot-workers/`)
110
110
  - **`codex-host/skills/pilot-workers/`**: the same playbook skill for Codex (installed to `$CODEX_HOME/skills/pilot-workers/`)
111
111
 
112
- `pilot-workers install <provider> on <host>` copies the skill into the host's skill directory and regenerates its worker table from that host's configuration; the doctrine itself carries no engine-specific knowledge. Removing the last provider deletes the skill, so a host with nothing configured has none and the planner does the work itself. (The v0.4.0 12-agents + 8-commands matrix is gone.)
112
+ `pilot-workers install <provider> on <host>` copies the skill into the host's skill directory and regenerates its worker table from that host's configuration; the doctrine itself carries no engine-specific knowledge. The deployed tree is a build artifact — every install re-renders it from the packaged template, so upgrading is `pip` upgrade + `pilot-workers install <host|all>` (hosts with nothing configured are untouched). Removing the last provider deletes the skill, so a host with nothing configured has none and the planner does the work itself. (The v0.4.0 12-agents + 8-commands matrix is gone.)
113
113
 
114
114
  Adding a new host: create `integrations/<name>-host/skills/pilot-workers/`, put whatever config your host needs, point it at `pilot-workers dispatch`. See `integrations/README.md`.
115
115
 
@@ -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.4"
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,92 @@ 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
+ # Example requests per mode, spliced into the generated description so the
499
+ # trigger has surface area that RESEMBLES real prompts — policy prose shares
500
+ # no tokens with an actual request like asking to trace a flow. Bilingual
501
+ # because users prompt in Chinese too; only ROUTED modes get their examples
502
+ # advertised (the sibling rule to not advertising an unassigned mode's
503
+ # routing). No ": " anywhere — the description must stay a valid plain YAML
504
+ # scalar. resume is planner-internal recovery, so it has no user phrasing.
505
+ MODE_TRIGGER_EXAMPLES: dict[str, tuple[str, ...]] = {
506
+ "explore": ('"how does X work"', '"trace this flow"',
507
+ '"探索/梳理/读一下这块代码"'),
508
+ "code": ('"implement/fix/refactor X"', '"改一下/实现/修复"'),
509
+ "test": ('"run the tests"', '"跑一下测试"'),
510
+ "review": ('"review this change"', '"review 一下这次改动"'),
511
+ }
512
+
513
+
514
+ def render_trigger_sentence(provider_keys: list[str],
515
+ modes: dict[str, str]) -> str:
516
+ """The frontmatter's trigger clause, generated from the host's ROUTING.
517
+
518
+ This is the only part of the skill a model sees before deciding whether to
519
+ load it, so it decides what the whole feature can do. Two failure modes are
520
+ on record, both found live:
521
+
522
+ - v0.5.2 read "Trigger when the user names a worker provider — ds, kimi-k3",
523
+ the most specific clause in the description and therefore the operative
524
+ one — so "explore this codebase" never loaded the skill, and the
525
+ mode -> provider table (which lives in the BODY) was never read.
526
+ - v0.5.3 routed by mode but left the judgment open: the surrounding static
527
+ text still said "worth delegating ... Not for small tweaks", so a small
528
+ exploration was judged not worth a worker and the skill never loaded.
529
+ Routing is a MANDATE — ``install ... for <mode>`` is the user making the
530
+ delegation decision once; the sentence must close the per-task
531
+ re-judgment, and the only stated override is explicit user input (the
532
+ top of the priority chain: user input, then config, then do-it-yourself
533
+ fallback).
534
+
535
+ Also load-bearing: NO unquoted colon-space anywhere in the sentence. The
536
+ clause is spliced into a plain YAML scalar, and a mid-value ": " is a
537
+ scanner error in strict YAML — v0.5.3's "Route by MODE, not by name:
538
+ explore" made the whole deployed frontmatter unparseable. A host with no
539
+ assignments routes nothing and must not claim otherwise.
540
+ """
541
+ names = ", ".join(provider_keys)
542
+ routed: dict[str, list[str]] = {}
543
+ for mode, key in sorted(modes.items()):
544
+ if key in provider_keys:
545
+ routed.setdefault(key, []).append(mode)
546
+ if not routed:
547
+ return (f"No mode is routed on this host yet, so nothing is delegated "
548
+ f"automatically; naming a provider ({names}) triggers this skill.")
549
+ parts = []
550
+ for key, mode_list in sorted(routed.items()):
551
+ head = ", ".join(sorted(mode_list))
552
+ examples = ", ".join(
553
+ ex for m in sorted(mode_list)
554
+ for ex in MODE_TRIGGER_EXAMPLES.get(m, ()))
555
+ suffix = f" (e.g. {examples})" if examples else ""
556
+ parts.append(f"{head} \u2192 {key}{suffix}")
557
+ groups = "; ".join(parts)
558
+ return (f"This host routes work by mode — {groups}. A request for a routed "
559
+ f"kind of work MUST be dispatched through this skill to its "
560
+ f"assigned provider, even when the task looks too small to be "
561
+ f"worth a worker. Never do routed work in this session and never "
562
+ f"weigh whether delegation is worth it — the `install ... for "
563
+ f"<mode>` that created the route already made that decision. The "
564
+ f"route also binds your own mid-task moves — realizing you need "
565
+ f"to read code to understand it, write code, run tests, or "
566
+ f"review a diff means dispatching, not doing it here; only "
567
+ f"verifying a known location (a cited file, a returned diff or "
568
+ f"verdict) stays local. Explicit user input is the only override "
569
+ f"— naming a provider ({names}) selects it, and an explicit "
570
+ f"request to do the work here keeps it here.")
571
+
572
+
498
573
  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.
574
+ provider_keys: list[str],
575
+ modes: dict[str, str] | None = None) -> str:
576
+ """Fill the frontmatter trigger token.
501
577
 
502
578
  Pure string function: a host's deployed skill names exactly the providers
503
579
  configured for it, so "use <provider>" reaches the skill without leaking
504
580
  the names of providers the host never got.
505
581
  """
506
- return skill_text.replace(TRIGGER_PLACEHOLDER, ", ".join(provider_keys))
582
+ return skill_text.replace(
583
+ TRIGGER_PLACEHOLDER, render_trigger_sentence(provider_keys, modes or {}))
507
584
 
508
585
 
509
586
  def _frontmatter_block(text: str) -> str | None:
@@ -527,35 +604,27 @@ def _packaged_skill_text(host: str) -> str:
527
604
  / "SKILL.md").read_text(encoding="utf-8")
528
605
 
529
606
 
530
- def _render_deployed_skill(installs: dict, host: str, base_text: str) -> str:
531
- """The deployed SKILL.md text: worker region + trigger list from config.
607
+ def _render_deployed_skill(installs: dict, host: str) -> str:
608
+ """The deployed SKILL.md text, rendered from the PACKAGED template.
532
609
 
533
- ``base_text`` supplies the hand-editable doctrine, so hand edits outside
534
- the markers and the frontmatter survive a refresh. The frontmatter is
535
- re-derived from package data on every render: the trigger token is
536
- consumed by the first deploy, so re-substituting ``base_text`` alone
537
- could never drop a provider that was uninstalled later.
610
+ The deployed tree is a build artifact: every render starts from package
611
+ data plus the recorded config, so a packaged doctrine update propagates on
612
+ the next install of any form. It used to rebase on the already-deployed
613
+ file to preserve hand edits which meant a package upgrade could never
614
+ deliver a body update to an existing deployment, while the frontmatter,
615
+ the generated region and every sibling file were rewritten anyway: half a
616
+ promise, and the delivered half was permanent staleness. Custom doctrine
617
+ belongs in the packaged template, where it survives upgrades properly.
618
+
619
+ A packaged template without a marker pair makes ``apply_generated_region``
620
+ raise loudly: that is a packaging bug, never an upgrade state.
538
621
  """
539
622
  provider_keys = host_providers(installs, host)
540
- region = render_worker_region(provider_keys, host_modes(installs, host))
541
- try:
542
- out = apply_generated_region(base_text, region)
543
- except RuntimeError:
544
- # No usable marker pair in the deployed text. That is the normal state
545
- # when upgrading from a release whose skill predates the markers, and it
546
- # also covers a deployment whose markers were lost to hand-editing —
547
- # they are HTML comments, invisible in rendered markdown. Fall back to
548
- # the packaged text, which always carries them. Hand edits in the old
549
- # file cannot be preserved here: there is no boundary left to preserve
550
- # them against.
551
- out = apply_generated_region(_packaged_skill_text(host), region)
552
- new_front = _frontmatter_block(
553
- substitute_trigger_placeholder(_packaged_skill_text(host),
554
- provider_keys))
555
- old_front = _frontmatter_block(out)
556
- if new_front is not None and old_front is not None:
557
- out = new_front + out[len(old_front):]
558
- return out
623
+ modes = host_modes(installs, host)
624
+ region = render_worker_region(provider_keys, modes)
625
+ text = substitute_trigger_placeholder(
626
+ _packaged_skill_text(host), provider_keys, modes)
627
+ return apply_generated_region(text, region)
559
628
 
560
629
 
561
630
  def _host_config_entry(installs: dict, host: str) -> dict:
@@ -739,33 +808,31 @@ def _deployed_skill_path(entry) -> Path | None:
739
808
  return None
740
809
 
741
810
 
742
- def _splice_region_into_deployed(installs: dict, host: str) -> None:
743
- """Regenerate the worker region of the host's already-deployed SKILL.md.
811
+ def _rerender_deployed_skill(installs: dict, host: str) -> None:
812
+ """Rewrite the host's already-deployed SKILL.md from the current render.
744
813
 
745
- Splices into the existing deployed file, so hand edits outside the markers
746
- AND outside the frontmatter survive (the frontmatter is re-derived from
747
- package data on every render see ``_render_deployed_skill``). No-op when
748
- the host has no deployed skill on record.
814
+ Full re-render from package data (see ``_render_deployed_skill``), written
815
+ to the RECORDED path this is the path routine provider install/uninstall
816
+ takes, which must not guess a target directory. Sibling files (modes/*.md)
817
+ are converged by the host-form install; this rewrites the one file whose
818
+ content depends on the recorded config. No-op when the host has no
819
+ deployed skill on record.
749
820
  """
750
821
  skill = _deployed_skill_path(installs.get(host))
751
822
  if skill is None or not skill.is_file():
752
823
  return
753
- # Atomic like every other skill write: this is the path routine provider
754
- # install/uninstall takes, and a truncating write_text here would leave a
755
- # crash as a half-written doctrine file that nothing detects or repairs.
756
- _write_skill_atomically(
757
- skill,
758
- _render_deployed_skill(installs, host, skill.read_text(encoding="utf-8")))
824
+ # Atomic like every other skill write: a truncating write_text here would
825
+ # leave a crash as a half-written doctrine file nothing detects or repairs.
826
+ _write_skill_atomically(skill, _render_deployed_skill(installs, host))
759
827
 
760
828
 
761
829
  def _deploy_skill_tree(installs: dict, host: str, target: Path | None) -> None:
762
- """Deploy/refresh the host's skill tree and splice the current region.
830
+ """Deploy/refresh the host's skill tree, rendered from package data.
763
831
 
764
832
  Records ``installed_at``/``package_version``/``files``/``created_dirs``
765
- on the host's manifest entry exactly as the host-install path does. On a
766
- refresh the region is spliced into the EXISTING deployed SKILL.md text,
767
- so hand edits outside the markers and the frontmatter survive a
768
- regeneration (the frontmatter is always re-derived from package data).
833
+ on the host's manifest entry exactly as the host-install path does. The
834
+ deployed tree is a build artifact: a refresh converges every file to the
835
+ current packaged render, so package upgrades propagate.
769
836
  """
770
837
  entry = installs[host]
771
838
  # A legacy v1/v2 nesting must be cleaned by whichever install touches the
@@ -778,18 +845,16 @@ def _deploy_skill_tree(installs: dict, host: str, target: Path | None) -> None:
778
845
  for key in [k for k in entry
779
846
  if k not in ("providers", "modes")]:
780
847
  del entry[key]
848
+ # The previously deployed SKILL.md path: not a render input (the render
849
+ # is pure package data + config), but the orphan sweep below must treat
850
+ # the previous location as in-bounds when --target moves a deployment.
781
851
  previous_skill = _deployed_skill_path(entry)
782
- previous_text: str | None = None
783
- if previous_skill is not None and previous_skill.is_file():
784
- previous_text = previous_skill.read_text(encoding="utf-8")
785
852
 
786
853
  # Render BEFORE anything on disk changes. Copying first and rendering after
787
854
  # means a render failure leaves the host with a half-updated skill — or none
788
855
  # at all on the purge path. Rendering needs no file, so nothing forces that
789
856
  # order.
790
- rendered = _render_deployed_skill(
791
- installs, host, previous_text if previous_text is not None
792
- else _packaged_skill_text(host))
857
+ rendered = _render_deployed_skill(installs, host)
793
858
 
794
859
  previous_files = set(entry.get("files") or [])
795
860
  result = install_host(host, target, skill_text=rendered)
@@ -1247,7 +1312,7 @@ def _install_provider_on_host(
1247
1312
  or not existing.is_file()):
1248
1313
  _deploy_skill_tree(installs, host, target)
1249
1314
  else:
1250
- _splice_region_into_deployed(installs, host)
1315
+ _rerender_deployed_skill(installs, host)
1251
1316
  if mode is None:
1252
1317
  if added:
1253
1318
  print(f" recorded: {provider_key} on {host}")
@@ -1308,18 +1373,9 @@ def main(argv: list[str] | None = None) -> int:
1308
1373
  print(f"{host}: no workers configured — nothing to install.")
1309
1374
  print(f" next: pilot-workers install <provider> on {host}")
1310
1375
  continue
1311
- # Capture the deployed text BEFORE the purge+recopy below, or the
1312
- # freshly copied package template becomes the splice base and any
1313
- # prose the user added outside the markers is lost.
1314
- previous_skill = _deployed_skill_path(previous)
1315
- previous_text: str | None = None
1316
- if previous_skill is not None and previous_skill.is_file():
1317
- previous_text = previous_skill.read_text(encoding="utf-8")
1318
1376
  # Render before destroying: a render failure must not leave the host
1319
1377
  # with a purged skill and nothing to replace it.
1320
- rendered = _render_deployed_skill(
1321
- installs, host, previous_text if previous_text is not None
1322
- else _packaged_skill_text(host))
1378
+ rendered = _render_deployed_skill(installs, host)
1323
1379
  # Migration / reinstall: purge every previous entry for this host
1324
1380
  # (v3/v4 flat reinstall OR legacy v1 __all__ + v2 providers), then
1325
1381
  # write a clean host-level v4 entry.
@@ -1342,9 +1398,8 @@ def main(argv: list[str] | None = None) -> int:
1342
1398
  new_entry["modes"] = previous["modes"]
1343
1399
  installs[host] = new_entry
1344
1400
  # The deployed skill reflects the recorded configuration; the copy
1345
- # above already wrote the text rendered from it (spliced into the
1346
- # previous text when there was one, so hand edits outside the
1347
- # markers and the frontmatter survive).
1401
+ # above already wrote the text rendered from package data plus
1402
+ # that configuration.
1348
1403
  # The manifest is committed ONCE, when the transaction exits — not
1349
1404
  # per host. A crash mid-``install all`` therefore records no hosts,
1350
1405
  # rather than some: the deployed files of the completed hosts are
@@ -1383,7 +1438,7 @@ def _uninstall_provider_on_host(provider_key: str, host: str) -> int:
1383
1438
  return 0
1384
1439
  entry = installs.get(host)
1385
1440
  if host_providers(installs, host):
1386
- _splice_region_into_deployed(installs, host)
1441
+ _rerender_deployed_skill(installs, host)
1387
1442
  elif isinstance(entry, dict) and isinstance(entry.get("files"), list):
1388
1443
  # That was the last provider: purge the deployed skill and drop
1389
1444
  # the host's manifest entry entirely.
@@ -1420,7 +1475,7 @@ def _uninstall_default_on_host(mode: str, host: str) -> int:
1420
1475
  try:
1421
1476
  with manifest_transaction(delete_when_empty=True) as installs:
1422
1477
  if clear_host_mode(installs, host, mode):
1423
- _splice_region_into_deployed(installs, host)
1478
+ _rerender_deployed_skill(installs, host)
1424
1479
  print(f" removed: {mode} assignment on {host}")
1425
1480
  else:
1426
1481
  print(f"note: no provider assigned to mode {mode!r} on {host}")
@@ -0,0 +1,148 @@
1
+ ---
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. {{PILOT_PROVIDER_TRIGGERS}}
4
+ ---
5
+
6
+ # pilot-workers Playbook
7
+
8
+ You are the planner. Workers are separate OpenCode processes that **cannot see
9
+ this conversation**. Planning, task decomposition, and final verification stay
10
+ with you; workers get only settled decisions.
11
+
12
+ ## When this skill applies
13
+
14
+ Two kinds of moments route here; match either, then choose the provider by
15
+ the priority chain below.
16
+
17
+ | Moment | Mode |
18
+ | --- | --- |
19
+ | "how does X work" / "trace this flow" / "探索、梳理、理解、读一下这块代码" — or YOU need to read code to build understanding before planning | explore |
20
+ | "implement/fix/refactor X" / "改一下、实现、修复" — or the plan is settled and edits would begin | code |
21
+ | "run the tests" / "跑一下测试" — or you want a suite run to verify a change | test |
22
+ | "review this change" / "review 一下这次改动" — or a rewrite-scale diff just landed | review |
23
+ | a prior dispatch failed or stopped short | resume |
24
+
25
+ The self-initiated rows draw the line by PURPOSE, never by size: reading to
26
+ LEARN (trace a flow, understand a module, gather context for a change) is an
27
+ explore dispatch no matter how few files; verifying a KNOWN location (a cited
28
+ file:line, a returned diff, a verdict) is local reading and needs no worker.
29
+
30
+ ## Quick Reference
31
+
32
+ ```bash
33
+ pilot-workers template <mode> > /tmp/<provider>-<mode>-<slug>-<timestamp>.md # fill it in — self-contained
34
+ pilot-workers dispatch --provider <key> --mode <mode> --workdir "$PWD" --task-file <file> # background Bash
35
+ ```
36
+
37
+ **Choose `<key>` by priority.** (1) Explicit user input always wins: a provider
38
+ the user names is used even where the Workers table assigns another, and an
39
+ explicit request to do the work here ("don't dispatch", "do it yourself") keeps
40
+ it in this session. (2) Otherwise the **Workers table** at the bottom of this
41
+ file decides: a routed mode is ALWAYS dispatched to its assigned provider —
42
+ never done in this session, and never weighed for whether it is worth a worker.
43
+ `install ... for <mode>` was the user making that economics decision once; it
44
+ is not yours to remake per task. If a routed dispatch seems wasteful, dispatch
45
+ anyway and say so afterwards — the user can
46
+ `pilot-workers uninstall for <mode> on <host>`. (3) **A mode with no provider
47
+ assigned is not an invitation to guess: do it yourself, in this session.** No
48
+ assignment means the user never delegated that kind of work; picking a worker
49
+ by its advertised strengths would delegate something they chose to keep. If a
50
+ mode looks worth delegating, say so and let them run
51
+ `pilot-workers install <provider> on <host> for <mode>`. Never dispatch to a
52
+ provider absent from the table: it was not configured here.
53
+
54
+ **Then Read `modes/<mode>.md`** (next to this file in the skill directory)
55
+ before writing the task file — it is that mode's task recipe and verification
56
+ contract. Never dispatch a mode whose playbook you have not read in this
57
+ session.
58
+
59
+ - stdout is exactly two JSON lines: first `worker_runner.started` (record
60
+ run_id and log paths), last `worker_runner.verdict`. **The completion signal
61
+ = the background Bash exiting on its own.** Never poll by process name;
62
+ never judge from the shared log (`latest.log` is for humans only). Fewer than
63
+ two lines means the run died before starting — read its stderr, because an
64
+ early failure (missing credential, runner not installed) emits no JSON at all.
65
+ - **Dispatch from the main session only, in a background Bash.** Subagents die
66
+ at turn end and their background tasks are never harvested — never dispatch
67
+ from a subagent, never "wait" by ending your turn.
68
+ - Consumption contract: read `verdict.result` first (the parsed PILOT_RESULT
69
+ block). Read `final_text_path` at most once, only when
70
+ `parse_state != "parsed"` or a finding requires surrounding context. Never
71
+ auto-redispatch on parse failure alone.
72
+ - Resume-first recovery: on failure or non-convergence, resume with
73
+ `--session <session_id>` + `--run-id <resume_run_id>` from the original verdict
74
+ before considering a cold redispatch. A `credential missing` error is not a
75
+ worker failure and resume will not fix it — the provider has no API key yet:
76
+ tell the user to run
77
+ `pilot-workers install <provider> on <host> --global-key`, which prompts for
78
+ it. The key belongs to the provider, so it is configured once for every host.
79
+ Never handle a key yourself.
80
+
81
+ ## Verdict matrix
82
+
83
+ The `verdict` state string and the sibling flags `timed_out` / `idle_timed_out` /
84
+ `interrupted` are set independently — **check the flags even on `completed`**.
85
+
86
+ | state | meaning | action |
87
+ |---|---|---|
88
+ | `completed` | usable report (`parse_state` parsed, or long unstructured text) | consume `result` first; flags may still be set — say so |
89
+ | `step_capped_partial` | step cap hit | partial coverage; a parsed partial block is still salvaged into `result` — report the uncovered scope truthfully |
90
+ | `error` | summary-error or crash with no usable text | `stderr_tail` first; open `jsonl_path` only if it does not explain the death |
91
+ | `empty` | no usable output | same: `stderr_tail`, then `jsonl_path` if needed |
92
+
93
+ Never draw a conclusion without reading the evidence.
94
+
95
+ `duration_s` / `tokens` / `final_text_len` are for your next decision, not the
96
+ report: cost so far, and whether `final_text_path` is worth opening at all
97
+ (`parse_state: parsed` means `result` already has everything).
98
+
99
+ `parse_state` values: `parsed` (consume `result`), `malformed` (the worker DID
100
+ write a result block and it did not parse — the work is real, read
101
+ `final_text_path`), `unstructured` (a block that was cut off mid-write — treat as
102
+ partial), `unavailable` (no block at all — the worker ignored the contract).
103
+
104
+ Synthesized fanout verdicts carry `synthesized: true` + `reason`
105
+ (`crash|timeout|idle_timeout|interrupted`) + `stderr_tail`; `result` and
106
+ `final_text_path` are null. Consume rule: synthesized → read `reason` +
107
+ `stderr_tail` only.
108
+
109
+ ## Task files
110
+
111
+ - The worker sees nothing but the task file: it must be **self-contained** —
112
+ explicit file paths, completion criteria, do-not-touch boundaries. Never
113
+ "that file" or "the module mentioned above".
114
+ - Unique `/tmp` names (`<provider>-<mode>-<slug>-<timestamp>.md`) so parallel
115
+ sessions never clobber each other.
116
+ - **Never any credentials** in a task file — it is sent verbatim to a
117
+ third-party endpoint. Dispatch refuses obvious key shapes and any key
118
+ configured on this machine, but that check **cannot catch every secret**:
119
+ it is a backstop, not permission to paste config. Reference a value by
120
+ env var and let the worker read it.
121
+ - **The worker can see the whole workdir.** Opening an `auth.json` or `.env` is
122
+ denied for both the shell and the read tool, but a path deny cannot stop a
123
+ recursive content search from returning a line out of one. If the project holds
124
+ live secrets in untracked files, dispatch with `--worktree`: a detached
125
+ worktree materialises tracked files only, so a gitignored `.env` is not there
126
+ at all — a boundary rather than a pattern.
127
+ - General discipline does not need to be written — dispatch injects
128
+ `prompts/*.md` automatically, including the `PILOT_RESULT` output block the
129
+ worker must end with.
130
+
131
+ <!--PILOT_GENERATED_BEGIN-->
132
+ <!--PILOT_GENERATED_END-->
133
+
134
+ ## Providers
135
+
136
+ Provider strengths and suitable modes live in the provider YAML registry,
137
+ not here — run `pilot-workers status <host>` when you need them (status
138
+ renders each provider's `strengths`, `suitable_modes`, and `notes` live).
139
+
140
+ ## Timeouts
141
+
142
+ - `--timeout <sec>` caps the whole job; the fanout parent watchdog
143
+ enforces it plus a grace period.
144
+ - **`--timeout 0` forfeits the parent watchdog** for that job (no deadline,
145
+ no silence kill); the child's `--idle-timeout` remains the only liveness
146
+ bound.
147
+ - `--timeout 0 --idle-timeout 0` together make a job **fully unbounded** —
148
+ avoid unless you are watching it yourself.
@@ -0,0 +1,41 @@
1
+ # Mode: code — dispatch playbook
2
+
3
+ (Read this before every code dispatch. The core SKILL.md holds the
4
+ priority chain, the red lines and the verdict contract; this file
5
+ holds the craft for this one mode.)
6
+
7
+ - **Shape the task; the dispatch decision is already made.** Routing (or the
8
+ user naming a provider) decided who does code work — what stays yours is
9
+ making the task carryable: settle every judgment call before dispatch
10
+ (workers get only settled decisions) and hand over specs, not questions. A
11
+ change that would need mid-course judgment at every step is a planning gap —
12
+ settle more, then dispatch.
13
+ - One worker call bundles at most 2-3 related fix points; split larger
14
+ batches into multiple workers (they can run in parallel).
15
+ - **Parallel code jobs require `--worktree`** so each worker gets an isolated
16
+ git worktree; one worker per background Bash; batch similar durations per
17
+ fanout and split mixed durations. Clean up with
18
+ `pilot-workers maintain worktrees remove <path>`.
19
+ - Verify once, and verify what is cheap to verify COMPLETELY. `git diff --stat`
20
+ plus `git status --short` settle one whole property for a few hundred tokens:
21
+ which files changed. Compare that set against the spec's whitelist — anything
22
+ outside it is a boundary violation, and a stray untracked file is worth
23
+ catching too. Reconcile the worker's own `FILES_CHANGED` against that set too — a
24
+ mismatch means it misreported its work, which colours everything else it
25
+ claimed. Also read `exit_code` / `session_id` / `steps` / `tool_errors`
26
+ from the verdict.
27
+ - **Do not sample the diff.** Reading a few hunks of a large change licenses
28
+ nothing about the rest; it costs attention and buys confidence you have not
29
+ earned. Correctness comes from the test run, from exercising the behaviour
30
+ end to end, and — for rewrite-scale diffs — from the cross-model review below.
31
+ Never treat the worker's completion claim as proof. **The test run goes to whichever provider is
32
+ assigned to `test` in the Workers table**, as a `test`-mode dispatch; running
33
+ it here would void that assignment. You still read the counts and failures and
34
+ decide what they mean.
35
+ - **Cross-model review for rewrite-scale diffs** (several hundred lines or more):
36
+ dispatch a *different* provider in review mode before verifying — the two
37
+ models' errors are uncorrelated, so the second catches the first's
38
+ systematic blind spots at cheap quota. Skip for small changes.
39
+ - Do not edit a worker's target files while it is running. Deletion,
40
+ migration, CI/keys/production config changes: do not dispatch; a human does
41
+ those.
@@ -0,0 +1,25 @@
1
+ # Mode: explore — dispatch playbook
2
+
3
+ (Read this before every explore dispatch. The core SKILL.md holds the
4
+ priority chain, the red lines and the verdict contract; this file
5
+ holds the craft for this one mode.)
6
+
7
+ Reading code is the bulk of token spend (read-to-write ratio roughly 50:1), so
8
+ exploration is a top dispatch candidate. Write the question straight into the
9
+ task file; do not read the code yourself first — that wastes the saving.
10
+
11
+ - List what to investigate item by item; pin the scope to specific
12
+ directories/file types to shrink roaming room.
13
+ - **Planning is the verification — do not sample.** Sampling a couple of
14
+ conclusions proves nothing about the rest and manufactures confidence in it.
15
+ Instead take the report into planning; the gaps surface as you use it:
16
+ a broad gap (wrong subsystem, missing the leads you need) goes back as a
17
+ rewritten explore dispatch, while three or five lines you still need — read
18
+ them here, a round-trip costs more than the read.
19
+ - Conclusions carrying no `file:line` are untrusted: you cannot plan on them and
20
+ cannot check them, so treat them as absent.
21
+ - Bring conclusions back **with their file:line references, verbatim** — the
22
+ main thread requires those references to plan; losing them ruins everything.
23
+ - Boundary: judgment/trade-off questions ("which approach is better") are your
24
+ job, not the worker's. Mixed explore+change tasks: explore first, plan,
25
+ then dispatch code.
@@ -0,0 +1,27 @@
1
+ # Mode: resume — dispatch playbook
2
+
3
+ (Read this before every resume dispatch. The core SKILL.md holds the
4
+ priority chain, the red lines and the verdict contract; this file
5
+ holds the craft for this one mode.)
6
+
7
+ - **Resume-first recovery.** When a run fails or does not converge, prefer
8
+ resume — it reuses the full prior-session context and saves minute-scale
9
+ round-trips versus a cold restart:
10
+
11
+ ```bash
12
+ pilot-workers dispatch --provider <key> --mode resume \
13
+ --session <session_id from the verdict> --run-id <resume_run_id from the verdict> \
14
+ --workdir <workdir from started> --task "Previous task incomplete: <what is missing, how to fix>"
15
+ ```
16
+
17
+ Take `--session` from the verdict and `--run-id` from its **`resume_run_id`**,
18
+ not its `run_id`: a resumed run gets a fresh `run_id` for its own log, so
19
+ resuming twice off `run_id` names a sandbox that never existed and fails as
20
+ "session expired". `resume_run_id` is the sandbox, and for a cold run the two
21
+ are equal — so read that one field either way. No template for resume — pass
22
+ `--task` with what remains and how to fix it.
23
+ - **Two obstacles, then take over**: if the worker hits the same obstacle
24
+ twice and still does not pass → the main session takes over and wraps up.
25
+ - The resume window == the sandbox retention window. A loud "session expired
26
+ past retention" error means the sandbox was reaped — redispatch cold. A
27
+ loud "run is still active" error means a live lock; do not force it.
@@ -0,0 +1,37 @@
1
+ # Mode: review — dispatch playbook
2
+
3
+ (Read this before every review dispatch. The core SKILL.md holds the
4
+ priority chain, the red lines and the verdict contract; this file
5
+ holds the craft for this one mode.)
6
+
7
+ - **Axis-splitting is your judgment call.** Fix 2-4 orthogonal axes
8
+ (correctness/boundary conditions, security, performance, consistency with
9
+ codebase conventions); one instance handles exactly one axis. Write each
10
+ axis as its own self-contained task file.
11
+ - **Parallel fanout recipe**: one job per axis, launched together —
12
+
13
+ ```bash
14
+ pilot-workers fanout --job <providerA>:review:/tmp/review-correctness-<ts>.md \
15
+ --job <providerB>:review:/tmp/review-security-<ts>.md
16
+ ```
17
+
18
+ (`--job PROVIDER:MODE:TASK_FILE`; mixing providers distributes load across
19
+ quotas.) Each job collects its verdict independently.
20
+ - **Aggregation is your job.** Merge and dedupe findings across axes, sort by
21
+ severity, report directly. **Verify every finding you intend to act on,
22
+ before acting** — not a sample of them. A review finding is a claim you will
23
+ answer by editing code, so a false positive turns into a wrong edit; open its
24
+ cited `file:line` and confirm the defect is real. Findings you are NOT acting
25
+ on need no verification: say they are unverified. Findings with no `file:line`
26
+ are untrusted.
27
+ - **Try to REFUTE each high before acting on it, and refute its fix too.**
28
+ Reviewers cannot run code (interpreters are denied in review mode), so their
29
+ findings are readings — plausible and sometimes wrong. Run the thing: the
30
+ input against the regex, the race with the lock disabled, the command in a
31
+ scratch copy. A proposed fix is a separate claim and has been wrong even when
32
+ the diagnosis was right. For a high you cannot settle by running something,
33
+ dispatch a *different* provider in review mode to argue against it.
34
+ - Review mode cannot edit; fixes go through your planning — small fixes
35
+ yourself, bulk mechanical fixes via a code dispatch.
36
+ - Axis too broad ("review the entire repo") → split axes first; unclear diff
37
+ baseline → clarify the two versions being compared first.
@@ -0,0 +1,15 @@
1
+ # Mode: test — dispatch playbook
2
+
3
+ (Read this before every test dispatch. The core SKILL.md holds the
4
+ priority chain, the red lines and the verdict contract; this file
5
+ holds the craft for this one mode.)
6
+
7
+ - The task states the exact test command, directory, preconditions, and any
8
+ known pre-existing failures. Run-and-gather only; fixes are your job.
9
+ - **Anti-false-positive**: counts + raw error text → trust it. "All passed"
10
+ with no counts → suspicious → read `jsonl_path` to see what command
11
+ actually ran, and redispatch if needed. Run `git status` to confirm the
12
+ worker left no junk changes behind.
13
+ - Bring counts and the failure list (test name, raw error text, `file:line`)
14
+ back verbatim; do not interpret root cause or propose fixes on the worker's
15
+ behalf.