claude-dev-env 1.88.1 → 1.90.0

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.
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ """Upload a file to a repo's durable 'artifacts' release and print its URL.
3
+
4
+ A durable GitHub post (issue, PR, comment, review) must not link a file under a
5
+ job scratch directory or worktree, because that scratch is cleaned soon after
6
+ the run while the post lives forever. Binary artifacts belong in a permanent
7
+ place instead. This tool ensures the repo has a prerelease tagged ``artifacts``,
8
+ uploads the given file under a timestamped asset name, and prints the permanent
9
+ download URL a post can safely link. The timestamped name keeps each upload a
10
+ distinct asset. An upload never overwrites an earlier one. A same-name collision
11
+ fails loudly instead of replacing the bytes an existing URL already serves.
12
+
13
+ Usage::
14
+
15
+ python3 gh_artifact_upload.py <file-path> <owner/repo>
16
+ -> https://github.com/<owner>/<repo>/releases/download/artifacts/20260707_140233_contact_sheet.png
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import datetime
23
+ import json
24
+ import shutil
25
+ import subprocess
26
+ import sys
27
+ import tempfile
28
+ from pathlib import Path
29
+
30
+ from dev_env_scripts_constants.gh_artifact_upload_constants import (
31
+ ARTIFACTS_RELEASE_NOTES,
32
+ ARTIFACTS_RELEASE_TAG,
33
+ ARTIFACTS_RELEASE_TITLE,
34
+ ASSET_CREATED_AT_JSON_KEY,
35
+ ASSET_NAME_TEMPLATE,
36
+ ASSET_NAME_TIMESTAMP_FORMAT,
37
+ ASSET_URL_JSON_KEY,
38
+ GH_BINARY_NAME,
39
+ NOTES_FILE_SUFFIX,
40
+ RELEASE_ASSETS_JSON_KEY,
41
+ UTF8_ENCODING,
42
+ )
43
+
44
+
45
+ class ArtifactUploadError(Exception):
46
+ """Raised when creating the release or uploading the asset fails."""
47
+
48
+
49
+ def _run_gh(all_arguments: list[str]) -> subprocess.CompletedProcess[str]:
50
+ return subprocess.run(
51
+ [GH_BINARY_NAME, *all_arguments],
52
+ capture_output=True,
53
+ text=True,
54
+ encoding=UTF8_ENCODING,
55
+ check=False,
56
+ )
57
+
58
+
59
+ def artifacts_release_exists(repository: str) -> bool:
60
+ """Return whether the durable artifacts release already exists in the repo.
61
+
62
+ Args:
63
+ repository: The ``owner/repo`` slug.
64
+
65
+ Returns:
66
+ True when ``gh release view`` finds the ``artifacts`` release.
67
+ """
68
+ completion = _run_gh(
69
+ [
70
+ "release",
71
+ "view",
72
+ ARTIFACTS_RELEASE_TAG,
73
+ "--repo",
74
+ repository,
75
+ "--json",
76
+ "tagName",
77
+ ]
78
+ )
79
+ return completion.returncode == 0
80
+
81
+
82
+ def ensure_artifacts_release(repository: str) -> None:
83
+ """Create the artifacts prerelease when the repo does not already have it.
84
+
85
+ Args:
86
+ repository: The ``owner/repo`` slug.
87
+
88
+ Raises:
89
+ ArtifactUploadError: When the release cannot be created.
90
+ """
91
+ if artifacts_release_exists(repository):
92
+ return
93
+ with tempfile.NamedTemporaryFile(
94
+ mode="w", suffix=NOTES_FILE_SUFFIX, delete=False, encoding=UTF8_ENCODING
95
+ ) as notes_file:
96
+ notes_file.write(ARTIFACTS_RELEASE_NOTES)
97
+ notes_path = notes_file.name
98
+ completion = _run_gh(
99
+ [
100
+ "release",
101
+ "create",
102
+ ARTIFACTS_RELEASE_TAG,
103
+ "--repo",
104
+ repository,
105
+ "--prerelease",
106
+ "--title",
107
+ ARTIFACTS_RELEASE_TITLE,
108
+ "--notes-file",
109
+ notes_path,
110
+ ]
111
+ )
112
+ Path(notes_path).unlink(missing_ok=True)
113
+ if completion.returncode != 0:
114
+ raise ArtifactUploadError(completion.stderr.strip())
115
+
116
+
117
+ def timestamped_asset_name(file_path: str) -> str:
118
+ """Return ``YYYYMMDD_HHMMSS_<basename>`` for the given file path.
119
+
120
+ Args:
121
+ file_path: The source file to be uploaded.
122
+
123
+ Returns:
124
+ The asset name that prefixes the basename with the current timestamp.
125
+ """
126
+ current_timestamp = datetime.datetime.now().strftime(ASSET_NAME_TIMESTAMP_FORMAT)
127
+ return ASSET_NAME_TEMPLATE.format(
128
+ timestamp=current_timestamp, basename=Path(file_path).name
129
+ )
130
+
131
+
132
+ def _uploaded_asset_download_url(repository: str) -> str:
133
+ """Read the just-uploaded asset's real download URL back from GitHub.
134
+
135
+ ::
136
+
137
+ upload "My Report.png" -> GitHub stores "..._My.Report.png"
138
+ read-back -> the URL GitHub serves for that sanitized name
139
+
140
+ GitHub sanitizes an asset filename on upload: spaces and other characters
141
+ become periods. The stored name, and so the download URL, can differ from
142
+ the local name. Reading the release back returns the URL GitHub actually
143
+ serves for the most recently created asset, which is the one just uploaded.
144
+
145
+ Args:
146
+ repository: The ``owner/repo`` slug.
147
+
148
+ Returns:
149
+ The browser download URL of the newest artifacts-release asset.
150
+
151
+ Raises:
152
+ ArtifactUploadError: When the read-back fails, the response cannot be
153
+ parsed, or no asset carries a download URL.
154
+ """
155
+ completion = _run_gh(
156
+ [
157
+ "release",
158
+ "view",
159
+ ARTIFACTS_RELEASE_TAG,
160
+ "--repo",
161
+ repository,
162
+ "--json",
163
+ RELEASE_ASSETS_JSON_KEY,
164
+ ]
165
+ )
166
+ if completion.returncode != 0:
167
+ raise ArtifactUploadError(completion.stderr.strip())
168
+ try:
169
+ parsed_release = json.loads(completion.stdout)
170
+ except json.JSONDecodeError as decode_error:
171
+ raise ArtifactUploadError(f"could not read release assets: {decode_error}")
172
+ all_assets = (
173
+ parsed_release.get(RELEASE_ASSETS_JSON_KEY, [])
174
+ if isinstance(parsed_release, dict)
175
+ else []
176
+ )
177
+ all_dated_assets = [
178
+ each_asset
179
+ for each_asset in all_assets
180
+ if isinstance(each_asset, dict) and each_asset.get(ASSET_CREATED_AT_JSON_KEY)
181
+ ]
182
+ if not all_dated_assets:
183
+ raise ArtifactUploadError("uploaded asset not found on read-back")
184
+ newest_asset = max(
185
+ all_dated_assets,
186
+ key=lambda each_asset: each_asset[ASSET_CREATED_AT_JSON_KEY],
187
+ )
188
+ asset_url = newest_asset.get(ASSET_URL_JSON_KEY)
189
+ if not isinstance(asset_url, str) or not asset_url:
190
+ raise ArtifactUploadError("uploaded asset carries no download URL on read-back")
191
+ return asset_url
192
+
193
+
194
+ def upload_artifact(file_path: str, repository: str) -> str:
195
+ """Upload a file to the artifacts release and return its permanent URL.
196
+
197
+ Args:
198
+ file_path: The source file to upload.
199
+ repository: The ``owner/repo`` slug.
200
+
201
+ Returns:
202
+ The permanent download URL for the uploaded asset.
203
+
204
+ Raises:
205
+ ArtifactUploadError: When the file is missing or the upload fails.
206
+ """
207
+ source_path = Path(file_path)
208
+ if not source_path.is_file():
209
+ raise ArtifactUploadError(f"file not found: {file_path}")
210
+ ensure_artifacts_release(repository)
211
+ asset_name = timestamped_asset_name(file_path)
212
+ with tempfile.TemporaryDirectory() as staging_directory:
213
+ staged_asset_path = Path(staging_directory) / asset_name
214
+ shutil.copyfile(source_path, staged_asset_path)
215
+ completion = _run_gh(
216
+ [
217
+ "release",
218
+ "upload",
219
+ ARTIFACTS_RELEASE_TAG,
220
+ str(staged_asset_path),
221
+ "--repo",
222
+ repository,
223
+ ]
224
+ )
225
+ if completion.returncode != 0:
226
+ raise ArtifactUploadError(completion.stderr.strip())
227
+ return _uploaded_asset_download_url(repository)
228
+
229
+
230
+ def main() -> int:
231
+ """Parse arguments, upload the file, and print the permanent asset URL.
232
+
233
+ Returns:
234
+ ``0`` when the upload succeeds, ``1`` when it fails.
235
+ """
236
+ parser = argparse.ArgumentParser(
237
+ description="Upload a file to a repo's durable 'artifacts' release."
238
+ )
239
+ parser.add_argument("file_path", help="Path to the file to upload.")
240
+ parser.add_argument(
241
+ "repository", metavar="owner/repo", help="Target GitHub repository slug."
242
+ )
243
+ parsed_arguments = parser.parse_args()
244
+ try:
245
+ asset_url = upload_artifact(
246
+ parsed_arguments.file_path, parsed_arguments.repository
247
+ )
248
+ except ArtifactUploadError as upload_error:
249
+ print(f"gh-artifact-upload failed: {upload_error}", file=sys.stderr)
250
+ return 1
251
+ print(asset_url)
252
+ return 0
253
+
254
+
255
+ if __name__ == "__main__":
256
+ sys.exit(main())
@@ -0,0 +1,205 @@
1
+ """Behavioral tests for gh_artifact_upload.py using a stubbed GitHub CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import types
9
+ from pathlib import Path
10
+
11
+ import pytest
12
+
13
+ _SCRIPTS_DIR = Path(__file__).resolve().parent.parent
14
+ if str(_SCRIPTS_DIR) not in sys.path:
15
+ sys.path.insert(0, str(_SCRIPTS_DIR))
16
+
17
+ import gh_artifact_upload as mod
18
+
19
+ _DEFAULT_READBACK_ASSET = {
20
+ "name": "20260707_140233_contact_sheet.png",
21
+ "url": (
22
+ "https://github.com/owner/repo/releases/download/artifacts/"
23
+ "20260707_140233_contact_sheet.png"
24
+ ),
25
+ "createdAt": "2026-07-07T14:02:33Z",
26
+ }
27
+
28
+
29
+ def _make_gh_stub(
30
+ recorded_calls: list[list[str]],
31
+ view_return_code: int,
32
+ all_readback_assets: list[dict[str, str]] | None = None,
33
+ ) -> object:
34
+ readback_assets = (
35
+ [_DEFAULT_READBACK_ASSET] if all_readback_assets is None else all_readback_assets
36
+ )
37
+
38
+ def fake_run(
39
+ all_command_arguments: list[str],
40
+ **_keyword_arguments: object,
41
+ ) -> types.SimpleNamespace:
42
+ recorded_calls.append(all_command_arguments)
43
+ is_release_view = "view" in all_command_arguments
44
+ is_asset_read_back = is_release_view and "assets" in all_command_arguments
45
+ if is_asset_read_back:
46
+ return types.SimpleNamespace(
47
+ returncode=0,
48
+ stdout=json.dumps({"assets": readback_assets}),
49
+ stderr="",
50
+ )
51
+ return_code = view_return_code if is_release_view else 0
52
+ return types.SimpleNamespace(returncode=return_code, stdout="", stderr="")
53
+
54
+ return fake_run
55
+
56
+
57
+ def test_timestamped_asset_name_prefixes_basename() -> None:
58
+ asset_name = mod.timestamped_asset_name(r"C:\stage\contact_sheet.png")
59
+ assert asset_name.endswith("_contact_sheet.png")
60
+ assert len(asset_name) > len("_contact_sheet.png")
61
+
62
+
63
+ def test_upload_artifact_returns_the_readback_url(
64
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
65
+ ) -> None:
66
+ source_file = tmp_path / "contact_sheet.png"
67
+ source_file.write_bytes(b"binary")
68
+ recorded_calls: list[list[str]] = []
69
+ monkeypatch.setattr(
70
+ subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0)
71
+ )
72
+
73
+ asset_url = mod.upload_artifact(str(source_file), "owner/repo")
74
+
75
+ assert asset_url == _DEFAULT_READBACK_ASSET["url"]
76
+ assert any("assets" in call for call in recorded_calls)
77
+
78
+
79
+ def test_upload_artifact_prints_the_sanitized_url_github_serves(
80
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
81
+ ) -> None:
82
+ source_file = tmp_path / "My Report.png"
83
+ source_file.write_bytes(b"binary")
84
+ sanitized_asset = {
85
+ "name": "20260707_140233_My.Report.png",
86
+ "url": (
87
+ "https://github.com/owner/repo/releases/download/artifacts/"
88
+ "20260707_140233_My.Report.png"
89
+ ),
90
+ "createdAt": "2026-07-07T14:02:33Z",
91
+ }
92
+ monkeypatch.setattr(
93
+ subprocess,
94
+ "run",
95
+ _make_gh_stub([], view_return_code=0, all_readback_assets=[sanitized_asset]),
96
+ )
97
+
98
+ asset_url = mod.upload_artifact(str(source_file), "owner/repo")
99
+
100
+ assert asset_url == sanitized_asset["url"]
101
+ assert " " not in asset_url
102
+
103
+
104
+ def test_upload_artifact_returns_the_newest_asset_url(
105
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
106
+ ) -> None:
107
+ source_file = tmp_path / "out.png"
108
+ source_file.write_bytes(b"binary")
109
+ older_asset = {
110
+ "name": "20260707_140000_out.png",
111
+ "url": "https://github.com/owner/repo/releases/download/artifacts/old.png",
112
+ "createdAt": "2026-07-07T14:00:00Z",
113
+ }
114
+ newest_asset = {
115
+ "name": "20260707_140233_out.png",
116
+ "url": "https://github.com/owner/repo/releases/download/artifacts/new.png",
117
+ "createdAt": "2026-07-07T14:02:33Z",
118
+ }
119
+ monkeypatch.setattr(
120
+ subprocess,
121
+ "run",
122
+ _make_gh_stub(
123
+ [], view_return_code=0, all_readback_assets=[older_asset, newest_asset]
124
+ ),
125
+ )
126
+
127
+ asset_url = mod.upload_artifact(str(source_file), "owner/repo")
128
+
129
+ assert asset_url == newest_asset["url"]
130
+
131
+
132
+ def test_upload_artifact_raises_when_asset_missing_on_readback(
133
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
134
+ ) -> None:
135
+ source_file = tmp_path / "out.png"
136
+ source_file.write_bytes(b"binary")
137
+ monkeypatch.setattr(
138
+ subprocess, "run", _make_gh_stub([], view_return_code=0, all_readback_assets=[])
139
+ )
140
+
141
+ with pytest.raises(mod.ArtifactUploadError):
142
+ mod.upload_artifact(str(source_file), "owner/repo")
143
+
144
+
145
+ def test_upload_artifact_uploads_to_tag_without_clobber(
146
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
147
+ ) -> None:
148
+ source_file = tmp_path / "out.png"
149
+ source_file.write_bytes(b"binary")
150
+ recorded_calls: list[list[str]] = []
151
+ monkeypatch.setattr(
152
+ subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0)
153
+ )
154
+
155
+ mod.upload_artifact(str(source_file), "owner/repo")
156
+
157
+ upload_call = next(call for call in recorded_calls if "upload" in call)
158
+ assert "artifacts" in upload_call
159
+ assert "--clobber" not in upload_call
160
+
161
+
162
+ def test_upload_artifact_creates_release_when_absent(
163
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
164
+ ) -> None:
165
+ source_file = tmp_path / "out.png"
166
+ source_file.write_bytes(b"binary")
167
+ recorded_calls: list[list[str]] = []
168
+ monkeypatch.setattr(
169
+ subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=1)
170
+ )
171
+
172
+ mod.upload_artifact(str(source_file), "owner/repo")
173
+
174
+ assert any("create" in call for call in recorded_calls)
175
+
176
+
177
+ def test_artifacts_release_exists_true_when_view_succeeds(
178
+ monkeypatch: pytest.MonkeyPatch,
179
+ ) -> None:
180
+ monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=0))
181
+ assert mod.artifacts_release_exists("owner/repo") is True
182
+
183
+
184
+ def test_artifacts_release_exists_false_when_view_fails(
185
+ monkeypatch: pytest.MonkeyPatch,
186
+ ) -> None:
187
+ monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=1))
188
+ assert mod.artifacts_release_exists("owner/repo") is False
189
+
190
+
191
+ def test_ensure_artifacts_release_skips_create_when_present(
192
+ monkeypatch: pytest.MonkeyPatch,
193
+ ) -> None:
194
+ recorded_calls: list[list[str]] = []
195
+ monkeypatch.setattr(
196
+ subprocess, "run", _make_gh_stub(recorded_calls, view_return_code=0)
197
+ )
198
+ mod.ensure_artifacts_release("owner/repo")
199
+ assert not any("create" in call for call in recorded_calls)
200
+
201
+
202
+ def test_upload_artifact_missing_file_raises(monkeypatch: pytest.MonkeyPatch) -> None:
203
+ monkeypatch.setattr(subprocess, "run", _make_gh_stub([], view_return_code=0))
204
+ with pytest.raises(mod.ArtifactUploadError):
205
+ mod.upload_artifact("does_not_exist_9f3a.png", "owner/repo")
package/skills/CLAUDE.md CHANGED
@@ -26,8 +26,6 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
26
26
  - `implement` — structured implementation from an existing plan packet
27
27
  - `bdd-protocol` — BDD depth: Example Mapping, scenario quality, outside-in layout
28
28
  - `verified-build` — build + test loop that gates on a verifier verdict
29
- - `advisor` — turns the session into the advisor-orchestrator: it spawns executor subagents to do the code edits and test runs, and answers a blocked executor with a plan, correction, or stop
30
- - `advisor-refresh` — sub-skill fired by the `/advisor` loop to re-assert the executor-advisor discipline mid-run
31
29
 
32
30
  **PR review and convergence**
33
31
  - `autoconverge` — autonomous single-run workflow that drives a PR to ready
@@ -1,185 +0,0 @@
1
- ---
2
- name: advisor
3
- description: >-
4
- Turns the session into the advisor-orchestrator — the user's sole interface,
5
- which routes execution through workflow-backed agent spawns with the required
6
- agent type and model for each work category. Executors do the code editing,
7
- verification, script driving, PR descriptions, and searches; the advisor
8
- answers blockers with one of three brief signals — a plan, a correction, or
9
- a stop. The advisor never edits code or runs tests itself. Caps consultations
10
- per task (default 5), reuses warm workflow agents before spawning new ones,
11
- and re-asserts the discipline every 20 minutes through the /advisor-refresh
12
- loop. Adapts Anthropic's advisor strategy to Claude Code. Triggers:
13
- '/advisor', 'advisor strategy', 'run with an advisor', 'executor-advisor
14
- mode', 'advisor enforcement', 'agent routing'.
15
- ---
16
-
17
- # Advisor Strategy
18
-
19
- ## Principle
20
-
21
- A cost-effective executor runs the primary work — it calls tools, reads
22
- results, and iterates — while a stronger model advises only at the hard
23
- decisions. Source: Anthropic's advisor strategy,
24
- https://claude.com/blog/the-advisor-strategy.
25
-
26
- Under this skill the session is the advisor-orchestrator. In Claude Code the
27
- user always talks to the session and never to a subagent, so the session is
28
- the user's sole interface: all user-facing communication flows through it. It
29
- spawns and resumes executor subagents — `clean-coder` and the like — and those
30
- executors do every bit of the execution: the code edits, the build runs, the
31
- test runs. The advisor drives the plan and answers the executors when they get
32
- stuck.
33
-
34
- This is the advisor-lite shape: the blog's pure pairing keeps the advisor
35
- tool-less and hidden from the user, but Claude Code routes every user message
36
- through the session, so the advisor here stays the user's interface while
37
- keeping execution out of its own hands. Consultations are capped (default 5
38
- per task) — the same guard the API's `max_uses` gives a tool.
39
-
40
- ## Gotchas
41
-
42
- - **Double invocation duplicates the reminder loop.** A second `/advisor` in
43
- the same session schedules a second refresh loop. Check whether the loop is
44
- already running before you schedule one (see the invocation guard in
45
- Process step 1).
46
- - **The advisor never executes.** The moment it edits a file or runs the tests
47
- itself, the pairing breaks and the executor's warm context is wasted. Hand
48
- every code edit and every build or test run to an executor; keep the
49
- advisor's own tool use to orchestration and light verification reads.
50
- - **Flat ad hoc spawns bypass routing.** Every execution task goes through a
51
- workflow-backed spawn or workflow resume so the required agent type, model,
52
- prompt packet, and sidecar metadata stay attached to the work.
53
- - **Wrong agent or model is an enforcement failure.** If a task category maps
54
- to `clean-coder` on `opus`, a `general-purpose` Sonnet spawn is not a cost
55
- optimization; it is the wrong executor for the contract.
56
- - **Resuming an unnamed background agent needs its agentId.** A background
57
- spawn returns an `agentId` (format `a...-...`); keep it so `SendMessage` can
58
- reach that agent later. A named agent is reachable by name.
59
- - **Consultations past the cap signal a scoping problem.** When five
60
- consultations do not clear the blocker, the task needs re-scoping or a
61
- hand-off to the user — not a sixth round of advice.
62
-
63
-
64
- ## Process
65
-
66
- 1. **Invocation guard (once per session).** Before scheduling anything, check
67
- whether the refresh loop is already running this session. If it is, skip
68
- straight to orchestration — do not schedule a second loop.
69
-
70
- 2. **Register the discipline reminder.** By default, schedule it with
71
- `ScheduleWakeup` at `delaySeconds: 1200`, prompt `/advisor-refresh`, where
72
- each refresh re-schedules the next one — a 1200-second wakeup costs one
73
- prompt-cache miss per firing and nothing more (see Gotchas). The loop
74
- mechanism (`/loop 20m /advisor-refresh`) is the escape hatch when
75
- `ScheduleWakeup` is not available. Either way the reminder is the
76
- enforcement surface: each firing re-asserts the discipline while the run is
77
- in flight.
78
-
79
- 3. **Orchestrate the task.** Hold the plan and the user conversation. Execute
80
- workflow-backed spawns or resumes using the routing table below, and keep
81
- driving while they work. Your own tool use stays orchestration and light
82
- verification reads. Keep your task list updated religiously.
83
-
84
- ## Workflow Agent Routing
85
-
86
- Every delegated task runs through a workflow-backed agent invocation. Do not
87
- spawn a flat subagent directly when a workflow invocation or workflow resume is
88
- available.
89
-
90
- | Work | Agent type | Model |
91
- |---|---|---|
92
- | Feature, bug, and refactor coding | `clean-coder` | `opus` |
93
- | Verification passes | `code-verifier` | `opus` |
94
- | Script runs, GitHub posting, and backfill driving | `general-purpose` runner | `sonnet` |
95
- | PR descriptions | `pr-description-writer` | `sonnet`, with file-list grounding check |
96
- | Fan-out searches and checklist verification reads | `Explore` | `haiku`; use `sonnet` when judgment-heavy |
97
- | Escalated blockers, plan-level design, and advisor signals | `code-advisor` or this session | Fable preferred; opus if fable is unavailable. |
98
-
99
- Routing rules:
100
-
101
- - Use a workflow invocation or resume for each row above. The workflow prompt
102
- must name the selected agent type, model, work category, task scope, and
103
- expected output.
104
- - Resume a warm workflow agent before creating a new workflow run when the warm
105
- agent holds the relevant context.
106
- - `clean-coder` owns code edits. `code-verifier` owns verification. The same
107
- workflow agent never grades work it wrote.
108
- - PR-description workflows include the actual changed-file list in the prompt
109
- and verify the final body against that file list before posting or returning
110
- it.
111
- - Exploration workflows return file paths, line numbers, and direct evidence;
112
- they do not write code or mutate repo state.
113
-
114
- 4. **Executors consult at a hard decision.** Each executor's spawn prompt tells
115
- it to stop and message you — with the task, what it tried, and the exact
116
- blocker (plus any short code excerpt that helps) — when one of these holds:
117
- - It has tried the same problem twice or more and it still fails.
118
- - A decision changes the deliverable's scope or a contract that is hard to
119
- reverse.
120
- - Two constraints conflict and it cannot satisfy both.
121
- - It is unsure whether to stop or keep going.
122
-
123
- 5. **Answer with one signal.** On a consultation, reply with exactly one
124
- signal, brief (about 400 to 700 tokens):
125
- - **PLAN** — a different approach, as concrete ordered steps the executor
126
- can run. When a warm agent fits the plan, name which one to resume.
127
- - **CORRECTION** — the executor's approach is right, one thing is wrong;
128
- name the wrong step and the fix.
129
- - **STOP** — no path satisfies the task as assigned; say why so it can be
130
- reported upward.
131
- The executor resumes the moment your reply lands.
132
-
133
- A worked consultation:
134
-
135
- ```
136
- Executor → advisor
137
- Task: add a retry to the upload client.
138
- Tried: wrapped upload() in a three-attempt loop; the second attempt
139
- double-posts.
140
- Blocker: the server takes no idempotency key, so a retry after a timeout
141
- creates a duplicate record.
142
-
143
- Advisor → executor
144
- CORRECTION — the retry loop is right; the missing piece is a stable request
145
- id. Generate one client-side on the first attempt and send the same id on
146
- every retry, so the server treats the retries as one request.
147
- ```
148
-
149
- For a decision the advisor itself cannot settle, it may use a workflow
150
- escalation to the tool-less `code-advisor` agent for a second opinion — an
151
- optional escalation, not a required step.
152
-
153
- ## Agent reuse (non-negotiable)
154
-
155
- - **Resume before you spawn, always.** A warm agent carries its context and
156
- cached tokens; a fresh spawn starts cold and pays to rebuild both.
157
- Resume the existing workflow agent by name or `agentId` when it holds
158
- relevant context. Prefer that path every time an existing workflow agent
159
- matches the routing table.
160
- - **Spawn a fresh agent only when** no existing agent holds relevant context,
161
- or a genuine task switch needs a clean context.
162
- - **Name the agent to resume.** When you answer with a PLAN and a warm agent
163
- fits, say which agent to resume and where.
164
-
165
- ## Constraints
166
-
167
- - One `/advisor` per session; the invocation guard blocks a second reminder
168
- loop.
169
- - The advisor orchestrates and advises but never edits code or runs a build or
170
- test itself — executors do that.
171
- - Delegated execution uses workflow-backed agent invocations and follows the
172
- Workflow Agent Routing table exactly.
173
- - Consultations are capped at five per task by default. At the cap, re-scope
174
- or hand off; do not keep answering.
175
- - Reuse a warm agent over a cold spawn whenever one holds relevant context.
176
-
177
- ## File Index
178
-
179
- | File | Purpose |
180
- |---|---|
181
- | `SKILL.md` | Advisor strategy, workflow routing contract, consultation protocol, reuse rules, and constraints. |
182
-
183
- ## Folder Map
184
-
185
- - `SKILL.md` — complete advisor workflow instructions.
@@ -1,25 +0,0 @@
1
- ---
2
- name: advisor-refresh
3
- description: >-
4
- Fired by the /advisor loop reminder about every 20 minutes to re-assert the
5
- advisor discipline mid-run. A compressed restatement of /advisor: orchestrate
6
- rather than execute, answer a blocked executor with a plan, correction, or
7
- stop, and reuse warm agents before spawning new ones. Triggers:
8
- '/advisor-refresh'.
9
- ---
10
-
11
- # Advisor Refresh
12
-
13
- 1. **You are the advisor.** Orchestrate and hold the user conversation; spawn
14
- executor subagents to do all the work — every code edit and build or test
15
- run.
16
- 2. **An executor blocked twice on the same thing?** Answer it with one signal
17
- — a plan, a correction, or a stop — brief. Never take over the edit or the
18
- tests yourself.
19
- 3. **Resume before you spawn.** `SendMessage` an existing agent by name or
20
- `agentId` to reuse its warm context; prefer that over a cold spawn.
21
- 4. **Fresh spawn only for a genuine task switch.** No tool compacts or clears a
22
- subagent's context, so a clean context comes from a fresh spawn — never tell
23
- an agent to compact.
24
- 5. **Re-schedule the next refresh** (about 1200 seconds out) when the loop
25
- mechanism needs each firing to queue the following one.