claude-dev-env 2.7.1 → 2.9.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.
- package/CLAUDE.md +7 -1
- package/_shared/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/clean-coder.md +9 -19
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +335 -12
- package/docs/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +2 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/team-advisor-skill.md +14 -8
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/output-styles/CLAUDE.md +17 -0
- package/output-styles/caveman-agent.md +37 -0
- package/package.json +2 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/rules/code-standards.md +33 -7
- package/rules/eli11-replies.md +1 -1
- package/scripts/CLAUDE.md +3 -3
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -0
- package/scripts/spawn_grok_batch.py +591 -10
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +396 -0
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/CLAUDE.md +4 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +15 -9
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/runner-selection.md +40 -0
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
- package/skills/e-code-review/scripts/grok_code_review.py +221 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +10 -0
- package/skills/orchestrator/SKILL.md +9 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -41
- package/system-prompts/software-engineer.xml +6 -3
- package/skills/test_markdown_link_integrity.py +0 -107
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Create a fresh branch in an isolated git worktree under
|
|
2
|
+
"""Create a fresh branch in an isolated git worktree under a configured root.
|
|
3
3
|
|
|
4
4
|
::
|
|
5
5
|
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
Never runs ``git checkout -b`` in the caller's working tree. Fetches the base
|
|
11
11
|
ref, then ``git worktree add -b --no-track`` into
|
|
12
|
-
``<
|
|
13
|
-
|
|
12
|
+
``<configured-root>/<agent>/<branch>`` (default configured root:
|
|
13
|
+
``<repo-root>/.claude/worktrees``). Exit 0 prints success JSON; any failure
|
|
14
|
+
prints ``{"error": ...}`` and exits non-zero.
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
from __future__ import annotations
|
|
@@ -33,6 +34,8 @@ from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
|
|
|
33
34
|
AGENT_SLUG_PATTERN,
|
|
34
35
|
ALL_AGENT_DETECTION_MARKERS,
|
|
35
36
|
ALL_REPOSITORY_WORKTREE_ROOT_PARTS,
|
|
37
|
+
CLI_FLAG_WORKTREE_ROOT,
|
|
38
|
+
CLI_HELP_WORKTREE_ROOT,
|
|
36
39
|
DEFAULT_AGENT_SLUG,
|
|
37
40
|
DEFAULT_BASE_REF,
|
|
38
41
|
ERROR_AGENT_SLUG_INVALID,
|
|
@@ -41,6 +44,7 @@ from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
|
|
|
41
44
|
ERROR_CLI_ARGUMENTS,
|
|
42
45
|
ERROR_UNIQUE_PATH_EXHAUSTED,
|
|
43
46
|
ERROR_WORKTREE_PATH_OUTSIDE_ROOT,
|
|
47
|
+
ERROR_WORKTREE_ROOT_NOT_ABSOLUTE,
|
|
44
48
|
EXIT_CODE_FAILURE,
|
|
45
49
|
EXIT_CODE_SUCCESS,
|
|
46
50
|
FRESH_BRANCH_AGENT_ENV_VAR,
|
|
@@ -91,26 +95,79 @@ def _detect_agent_slug_from_environment() -> str:
|
|
|
91
95
|
return DEFAULT_AGENT_SLUG
|
|
92
96
|
|
|
93
97
|
|
|
94
|
-
def
|
|
95
|
-
|
|
98
|
+
def resolve_configured_worktree_root(
|
|
99
|
+
repo_root: Path,
|
|
100
|
+
maybe_worktree_root: str | None = None,
|
|
101
|
+
) -> Path:
|
|
102
|
+
"""Return the isolated root that every worktree path must stay under.
|
|
103
|
+
|
|
104
|
+
::
|
|
105
|
+
|
|
106
|
+
resolve_configured_worktree_root(Path("/srv/app"))
|
|
107
|
+
# ok: /srv/app/.claude/worktrees
|
|
108
|
+
resolve_configured_worktree_root(Path("/srv/app"), "/tmp/isolated")
|
|
109
|
+
# ok: /tmp/isolated
|
|
110
|
+
resolve_configured_worktree_root(Path("/srv/app"), "relative/path")
|
|
111
|
+
# flag: ValueError --worktree-root must be an absolute path
|
|
112
|
+
|
|
113
|
+
When ``maybe_worktree_root`` is omitted, the default root is the repository's
|
|
114
|
+
``.claude/worktrees`` directory. An explicit root must be absolute; it may
|
|
115
|
+
sit outside the repository.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
repo_root: Absolute path of the repository the branch comes from.
|
|
119
|
+
maybe_worktree_root: Optional absolute configured root, or None.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Resolved absolute configured root directory.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ValueError: When ``maybe_worktree_root`` is set and is not absolute.
|
|
126
|
+
"""
|
|
127
|
+
if maybe_worktree_root is None:
|
|
128
|
+
return repo_root.joinpath(*ALL_REPOSITORY_WORKTREE_ROOT_PARTS).resolve()
|
|
129
|
+
cleaned_worktree_root = maybe_worktree_root.strip()
|
|
130
|
+
if not cleaned_worktree_root:
|
|
131
|
+
return repo_root.joinpath(*ALL_REPOSITORY_WORKTREE_ROOT_PARTS).resolve()
|
|
132
|
+
configured_root = Path(cleaned_worktree_root)
|
|
133
|
+
if not configured_root.is_absolute():
|
|
134
|
+
raise ValueError(ERROR_WORKTREE_ROOT_NOT_ABSOLUTE)
|
|
135
|
+
return configured_root.resolve()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def resolve_agent_worktree_root(
|
|
139
|
+
repo_root: Path,
|
|
140
|
+
agent_slug: str,
|
|
141
|
+
maybe_worktree_root: str | None = None,
|
|
142
|
+
) -> Path:
|
|
143
|
+
"""Return the per-agent directory under the configured worktree root.
|
|
96
144
|
|
|
97
145
|
::
|
|
98
146
|
|
|
99
147
|
# repo_root=/srv/app, agent_slug=grok
|
|
100
148
|
resolve_agent_worktree_root(Path("/srv/app"), "grok")
|
|
101
149
|
# -> /srv/app/.claude/worktrees/grok
|
|
150
|
+
resolve_agent_worktree_root(
|
|
151
|
+
Path("/srv/app"), "grok", maybe_worktree_root="/tmp/isolated",
|
|
152
|
+
)
|
|
153
|
+
# -> /tmp/isolated/grok
|
|
102
154
|
|
|
103
|
-
|
|
104
|
-
|
|
155
|
+
Each agent keeps its own subdirectory under the configured root so concurrent
|
|
156
|
+
hosts do not share worktree folders.
|
|
105
157
|
|
|
106
158
|
Args:
|
|
107
159
|
repo_root: Absolute path of the repository the branch comes from.
|
|
108
160
|
agent_slug: Short host label (one path segment).
|
|
161
|
+
maybe_worktree_root: Optional absolute configured root, or None.
|
|
109
162
|
|
|
110
163
|
Returns:
|
|
111
164
|
Directory that should hold per-branch worktree folders.
|
|
112
165
|
"""
|
|
113
|
-
|
|
166
|
+
configured_root = resolve_configured_worktree_root(
|
|
167
|
+
repo_root,
|
|
168
|
+
maybe_worktree_root=maybe_worktree_root,
|
|
169
|
+
)
|
|
170
|
+
return configured_root / agent_slug
|
|
114
171
|
|
|
115
172
|
|
|
116
173
|
def resolve_unique_worktree_path(preferred_path: Path) -> Path:
|
|
@@ -170,38 +227,55 @@ def create_fresh_branch(
|
|
|
170
227
|
repo_path: Path,
|
|
171
228
|
agent_slug: str,
|
|
172
229
|
base_ref: str,
|
|
230
|
+
maybe_worktree_root: str | None = None,
|
|
173
231
|
) -> dict[str, str]:
|
|
174
232
|
"""Fetch base_ref and create an isolated worktree branch with no upstream.
|
|
175
233
|
|
|
176
234
|
::
|
|
177
235
|
|
|
178
236
|
create_fresh_branch("fix/x", Path("."), "grok", "origin/main")
|
|
237
|
+
# worktree under <repo>/.claude/worktrees/grok/fix/x
|
|
238
|
+
create_fresh_branch(
|
|
239
|
+
"fix/x", Path("."), "grok", "origin/main",
|
|
240
|
+
maybe_worktree_root="/tmp/isolated",
|
|
241
|
+
)
|
|
242
|
+
# worktree under /tmp/isolated/grok/fix/x
|
|
179
243
|
|
|
180
|
-
The caller's working tree is never checked out. The
|
|
181
|
-
|
|
182
|
-
|
|
244
|
+
The caller's working tree is never checked out. The configured root is
|
|
245
|
+
validated before any git fetch so a relative ``--worktree-root`` fails
|
|
246
|
+
closed with no network I/O. The worktree lands under
|
|
247
|
+
``<configured-root>/<agent>/<branch>``, suffixed ``-2``, ``-3``, … when
|
|
248
|
+
that path is already taken. Every allocated path must resolve beneath the
|
|
249
|
+
configured root.
|
|
183
250
|
|
|
184
251
|
Args:
|
|
185
252
|
branch_name: Branch to create; must be a safe relative path.
|
|
186
253
|
repo_path: Any path inside the repository the branch comes from.
|
|
187
254
|
agent_slug: Short host label naming the worktree-root subdirectory.
|
|
188
255
|
base_ref: Ref the branch starts from, such as ``origin/main``.
|
|
256
|
+
maybe_worktree_root: Optional absolute configured root, or None for
|
|
257
|
+
the default ``<repo>/.claude/worktrees``.
|
|
189
258
|
|
|
190
259
|
Returns:
|
|
191
260
|
The success payload: branch, worktree_path, base_ref, base_commit,
|
|
192
261
|
agent, and repo_root.
|
|
193
262
|
|
|
194
263
|
Raises:
|
|
195
|
-
ValueError: When the branch name or
|
|
264
|
+
ValueError: When the branch name, agent slug, or worktree root is unsafe.
|
|
196
265
|
RuntimeError: When git refuses the fetch or the worktree add.
|
|
197
266
|
"""
|
|
198
267
|
cleaned_branch = _require_safe_branch_name(branch_name)
|
|
199
268
|
normalized_agent_slug = _normalize_agent_slug(agent_slug)
|
|
200
|
-
|
|
201
|
-
|
|
269
|
+
repo_root = resolve_repo_root(repo_path)
|
|
270
|
+
configured_root = resolve_configured_worktree_root(
|
|
271
|
+
repo_root,
|
|
272
|
+
maybe_worktree_root=maybe_worktree_root,
|
|
202
273
|
)
|
|
274
|
+
resolved_base_ref, base_commit = _fetch_resolved_base(repo_root, base_ref)
|
|
203
275
|
worktree_path = _allocate_worktree_path(
|
|
204
|
-
cleaned_branch,
|
|
276
|
+
cleaned_branch,
|
|
277
|
+
normalized_agent_slug,
|
|
278
|
+
configured_root,
|
|
205
279
|
)
|
|
206
280
|
create_worktree_branch(
|
|
207
281
|
repo_root,
|
|
@@ -215,15 +289,11 @@ def create_fresh_branch(
|
|
|
215
289
|
)
|
|
216
290
|
|
|
217
291
|
|
|
218
|
-
def
|
|
219
|
-
repo_path: Path,
|
|
220
|
-
base_ref: str,
|
|
221
|
-
) -> tuple[str, Path, str]:
|
|
292
|
+
def _fetch_resolved_base(repo_root: Path, base_ref: str) -> tuple[str, str]:
|
|
222
293
|
resolved_base_ref = normalize_base_ref(base_ref)
|
|
223
|
-
repo_root = resolve_repo_root(repo_path)
|
|
224
294
|
fetch_base_ref(repo_root, resolved_base_ref)
|
|
225
295
|
base_commit = resolve_base_commit(repo_root, resolved_base_ref)
|
|
226
|
-
return resolved_base_ref,
|
|
296
|
+
return resolved_base_ref, base_commit
|
|
227
297
|
|
|
228
298
|
|
|
229
299
|
def main() -> int:
|
|
@@ -233,14 +303,7 @@ def main() -> int:
|
|
|
233
303
|
Process exit code (0 success, 1 failure).
|
|
234
304
|
"""
|
|
235
305
|
try:
|
|
236
|
-
|
|
237
|
-
agent_slug = resolve_agent_slug(parsed_arguments.agent)
|
|
238
|
-
success_payload = create_fresh_branch(
|
|
239
|
-
branch_name=parsed_arguments.branch_name,
|
|
240
|
-
repo_path=Path(parsed_arguments.repo).resolve(),
|
|
241
|
-
agent_slug=agent_slug,
|
|
242
|
-
base_ref=parsed_arguments.base,
|
|
243
|
-
)
|
|
306
|
+
success_payload = _create_fresh_branch_from_cli_arguments()
|
|
244
307
|
print(json.dumps(success_payload))
|
|
245
308
|
return EXIT_CODE_SUCCESS
|
|
246
309
|
except (ValueError, RuntimeError, OSError) as error:
|
|
@@ -248,6 +311,18 @@ def main() -> int:
|
|
|
248
311
|
return EXIT_CODE_FAILURE
|
|
249
312
|
|
|
250
313
|
|
|
314
|
+
def _create_fresh_branch_from_cli_arguments() -> dict[str, str]:
|
|
315
|
+
parsed_arguments = _parse_arguments()
|
|
316
|
+
agent_slug = resolve_agent_slug(parsed_arguments.agent)
|
|
317
|
+
return create_fresh_branch(
|
|
318
|
+
branch_name=parsed_arguments.branch_name,
|
|
319
|
+
repo_path=Path(parsed_arguments.repo).resolve(),
|
|
320
|
+
agent_slug=agent_slug,
|
|
321
|
+
base_ref=parsed_arguments.base,
|
|
322
|
+
maybe_worktree_root=parsed_arguments.worktree_root,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
251
326
|
def _require_safe_branch_name(branch_name: str) -> str:
|
|
252
327
|
cleaned_branch = branch_name.strip()
|
|
253
328
|
if not cleaned_branch:
|
|
@@ -257,19 +332,21 @@ def _require_safe_branch_name(branch_name: str) -> str:
|
|
|
257
332
|
|
|
258
333
|
|
|
259
334
|
def _allocate_worktree_path(
|
|
260
|
-
branch_name: str,
|
|
335
|
+
branch_name: str,
|
|
336
|
+
agent_slug: str,
|
|
337
|
+
configured_root: Path,
|
|
261
338
|
) -> Path:
|
|
262
|
-
agent_worktree_root =
|
|
339
|
+
agent_worktree_root = configured_root / agent_slug
|
|
263
340
|
agent_worktree_root.mkdir(parents=True, exist_ok=True)
|
|
264
341
|
preferred_path = agent_worktree_root / branch_name
|
|
265
|
-
|
|
342
|
+
_assert_path_is_under_configured_root(
|
|
266
343
|
candidate_path=preferred_path,
|
|
267
|
-
|
|
344
|
+
configured_root=configured_root,
|
|
268
345
|
)
|
|
269
346
|
worktree_path = resolve_unique_worktree_path(preferred_path)
|
|
270
|
-
|
|
347
|
+
_assert_path_is_under_configured_root(
|
|
271
348
|
candidate_path=worktree_path,
|
|
272
|
-
|
|
349
|
+
configured_root=configured_root,
|
|
273
350
|
)
|
|
274
351
|
return worktree_path
|
|
275
352
|
|
|
@@ -309,12 +386,12 @@ def _validate_branch_name_for_worktree_path(branch_name: str) -> None:
|
|
|
309
386
|
assert_git_accepts_branch_name(branch_name)
|
|
310
387
|
|
|
311
388
|
|
|
312
|
-
def
|
|
389
|
+
def _assert_path_is_under_configured_root(
|
|
313
390
|
candidate_path: Path,
|
|
314
|
-
|
|
391
|
+
configured_root: Path,
|
|
315
392
|
) -> None:
|
|
316
393
|
resolved_candidate = candidate_path.resolve()
|
|
317
|
-
resolved_root =
|
|
394
|
+
resolved_root = configured_root.resolve()
|
|
318
395
|
if resolved_candidate == resolved_root:
|
|
319
396
|
return
|
|
320
397
|
try:
|
|
@@ -342,13 +419,19 @@ def _parse_arguments() -> argparse.Namespace:
|
|
|
342
419
|
parser.add_argument(
|
|
343
420
|
"--agent",
|
|
344
421
|
default=None,
|
|
345
|
-
help="Host label for
|
|
422
|
+
help="Host label for <configured-root>/<agent>/ (default: detect from environment).",
|
|
346
423
|
)
|
|
347
424
|
parser.add_argument(
|
|
348
425
|
"--base",
|
|
349
426
|
default=DEFAULT_BASE_REF,
|
|
350
427
|
help=f"Base ref to fetch and branch from (default: {DEFAULT_BASE_REF}).",
|
|
351
428
|
)
|
|
429
|
+
parser.add_argument(
|
|
430
|
+
CLI_FLAG_WORKTREE_ROOT,
|
|
431
|
+
default=None,
|
|
432
|
+
dest="worktree_root",
|
|
433
|
+
help=CLI_HELP_WORKTREE_ROOT,
|
|
434
|
+
)
|
|
352
435
|
try:
|
|
353
436
|
return parser.parse_args()
|
|
354
437
|
except SystemExit as exit_error:
|
package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Constants for the fresh-branch worktree creator.
|
|
2
2
|
|
|
3
|
-
Groups: default refs, agent detection, path layout, JSON payload
|
|
4
|
-
exit codes, and unique-path suffix limits.
|
|
3
|
+
Groups: default refs, agent detection, path layout, CLI flags, JSON payload
|
|
4
|
+
keys, exit codes, and unique-path suffix limits.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -25,6 +25,12 @@ PATH_SEGMENT_PARENT = ".."
|
|
|
25
25
|
|
|
26
26
|
ALL_REPOSITORY_WORKTREE_ROOT_PARTS = (".claude", "worktrees")
|
|
27
27
|
|
|
28
|
+
CLI_FLAG_WORKTREE_ROOT = "--worktree-root"
|
|
29
|
+
CLI_HELP_WORKTREE_ROOT = (
|
|
30
|
+
"Absolute isolated root for worktrees "
|
|
31
|
+
"(default: <repo>/.claude/worktrees). Agent and branch nest under it."
|
|
32
|
+
)
|
|
33
|
+
|
|
28
34
|
MAXIMUM_UNIQUE_PATH_ATTEMPTS = 100
|
|
29
35
|
UNIQUE_PATH_SUFFIX_START = 2
|
|
30
36
|
|
|
@@ -60,7 +66,8 @@ ERROR_BRANCH_NAME_REQUIRED = "branch name is required"
|
|
|
60
66
|
ERROR_BRANCH_NAME_UNSAFE = (
|
|
61
67
|
"branch name must be a relative path without '.' or '..' segments"
|
|
62
68
|
)
|
|
63
|
-
ERROR_WORKTREE_PATH_OUTSIDE_ROOT = "worktree path escapes
|
|
69
|
+
ERROR_WORKTREE_PATH_OUTSIDE_ROOT = "worktree path escapes configured root: %s"
|
|
70
|
+
ERROR_WORKTREE_ROOT_NOT_ABSOLUTE = "--worktree-root must be an absolute path"
|
|
64
71
|
ERROR_CLI_ARGUMENTS = "invalid or missing command-line arguments"
|
|
65
72
|
ERROR_AGENT_SLUG_INVALID = "agent slug must be lowercase letters, digits, or hyphens"
|
|
66
73
|
ERROR_REPO_NOT_GIT = "path is not inside a git repository: %s"
|
|
@@ -17,9 +17,12 @@ if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
|
17
17
|
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
18
18
|
|
|
19
19
|
from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
|
|
20
|
+
ALL_REPOSITORY_WORKTREE_ROOT_PARTS,
|
|
21
|
+
CLI_FLAG_WORKTREE_ROOT,
|
|
20
22
|
DEFAULT_AGENT_SLUG,
|
|
21
23
|
DEFAULT_BASE_REF,
|
|
22
24
|
ERROR_CLI_ARGUMENTS,
|
|
25
|
+
ERROR_WORKTREE_ROOT_NOT_ABSOLUTE,
|
|
23
26
|
EXIT_CODE_FAILURE,
|
|
24
27
|
EXIT_CODE_SUCCESS,
|
|
25
28
|
FRESH_BRANCH_AGENT_ENV_VAR,
|
|
@@ -244,6 +247,40 @@ class TestResolveAgentSlug:
|
|
|
244
247
|
module.resolve_agent_slug("../evil")
|
|
245
248
|
|
|
246
249
|
|
|
250
|
+
class TestResolveConfiguredWorktreeRoot:
|
|
251
|
+
def should_default_to_repo_claude_worktrees(
|
|
252
|
+
self,
|
|
253
|
+
tmp_path: Path,
|
|
254
|
+
) -> None:
|
|
255
|
+
module = load_create_fresh_branch_module()
|
|
256
|
+
configured_root = module.resolve_configured_worktree_root(tmp_path)
|
|
257
|
+
expected_root = tmp_path.joinpath(*ALL_REPOSITORY_WORKTREE_ROOT_PARTS)
|
|
258
|
+
assert configured_root == expected_root.resolve()
|
|
259
|
+
|
|
260
|
+
def should_accept_an_absolute_root_outside_the_repo(
|
|
261
|
+
self,
|
|
262
|
+
tmp_path: Path,
|
|
263
|
+
) -> None:
|
|
264
|
+
module = load_create_fresh_branch_module()
|
|
265
|
+
external_root = (tmp_path / "external-isolated").resolve()
|
|
266
|
+
configured_root = module.resolve_configured_worktree_root(
|
|
267
|
+
tmp_path / "repo",
|
|
268
|
+
maybe_worktree_root=str(external_root),
|
|
269
|
+
)
|
|
270
|
+
assert configured_root == external_root
|
|
271
|
+
|
|
272
|
+
def should_reject_a_relative_worktree_root(
|
|
273
|
+
self,
|
|
274
|
+
tmp_path: Path,
|
|
275
|
+
) -> None:
|
|
276
|
+
module = load_create_fresh_branch_module()
|
|
277
|
+
with pytest.raises(ValueError, match="absolute path"):
|
|
278
|
+
module.resolve_configured_worktree_root(
|
|
279
|
+
tmp_path,
|
|
280
|
+
maybe_worktree_root="relative/worktrees",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
247
284
|
class TestResolveAgentWorktreeRoot:
|
|
248
285
|
def should_nest_under_the_repository_claude_worktrees_directory(
|
|
249
286
|
self,
|
|
@@ -263,6 +300,19 @@ class TestResolveAgentWorktreeRoot:
|
|
|
263
300
|
assert grok_root != claude_root
|
|
264
301
|
assert grok_root.parent == claude_root.parent
|
|
265
302
|
|
|
303
|
+
def should_nest_agent_under_an_explicit_configured_root(
|
|
304
|
+
self,
|
|
305
|
+
tmp_path: Path,
|
|
306
|
+
) -> None:
|
|
307
|
+
module = load_create_fresh_branch_module()
|
|
308
|
+
external_root = (tmp_path / "isolated-root").resolve()
|
|
309
|
+
agent_root = module.resolve_agent_worktree_root(
|
|
310
|
+
tmp_path / "repo",
|
|
311
|
+
"grok",
|
|
312
|
+
maybe_worktree_root=str(external_root),
|
|
313
|
+
)
|
|
314
|
+
assert agent_root == external_root / "grok"
|
|
315
|
+
|
|
266
316
|
|
|
267
317
|
class TestResolveUniqueWorktreePath:
|
|
268
318
|
def should_return_preferred_when_missing(self, tmp_path: Path) -> None:
|
|
@@ -358,6 +408,146 @@ class TestBranchNamePathSafety:
|
|
|
358
408
|
assert not (tmp_path / "unused-repo").exists()
|
|
359
409
|
|
|
360
410
|
|
|
411
|
+
class TestConfiguredWorktreeRootIntegration:
|
|
412
|
+
def should_create_worktree_under_explicit_absolute_root(
|
|
413
|
+
self,
|
|
414
|
+
tmp_path: Path,
|
|
415
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
416
|
+
) -> None:
|
|
417
|
+
module = load_create_fresh_branch_module()
|
|
418
|
+
repository_path = build_repo_with_origin(tmp_path / "repo")
|
|
419
|
+
configured_root = (tmp_path / "isolated-worktrees").resolve()
|
|
420
|
+
caller_head_before = read_head_branch(repository_path)
|
|
421
|
+
caller_commit_before = read_head_commit(repository_path)
|
|
422
|
+
success_payload = module.create_fresh_branch(
|
|
423
|
+
branch_name="fix/external-root",
|
|
424
|
+
repo_path=repository_path,
|
|
425
|
+
agent_slug="grok",
|
|
426
|
+
base_ref=DEFAULT_BASE_REF,
|
|
427
|
+
maybe_worktree_root=str(configured_root),
|
|
428
|
+
)
|
|
429
|
+
worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
|
|
430
|
+
assert worktree_path == configured_root / "grok" / "fix" / "external-root"
|
|
431
|
+
assert worktree_path.is_dir()
|
|
432
|
+
assert worktree_path.resolve().is_relative_to(configured_root)
|
|
433
|
+
assert not (
|
|
434
|
+
repository_path.resolve()
|
|
435
|
+
/ ".claude"
|
|
436
|
+
/ "worktrees"
|
|
437
|
+
/ "grok"
|
|
438
|
+
/ "fix"
|
|
439
|
+
/ "external-root"
|
|
440
|
+
).exists()
|
|
441
|
+
assert read_head_branch(repository_path) == caller_head_before
|
|
442
|
+
assert read_head_commit(repository_path) == caller_commit_before
|
|
443
|
+
|
|
444
|
+
def should_suffix_when_path_exists_under_configured_root(
|
|
445
|
+
self,
|
|
446
|
+
tmp_path: Path,
|
|
447
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
448
|
+
) -> None:
|
|
449
|
+
module = load_create_fresh_branch_module()
|
|
450
|
+
repository_path = build_repo_with_origin(tmp_path / "repo")
|
|
451
|
+
configured_root = (tmp_path / "isolated-worktrees").resolve()
|
|
452
|
+
occupied_path = configured_root / "claude" / "fix" / "collision"
|
|
453
|
+
occupied_path.mkdir(parents=True)
|
|
454
|
+
success_payload = module.create_fresh_branch(
|
|
455
|
+
branch_name="fix/collision",
|
|
456
|
+
repo_path=repository_path,
|
|
457
|
+
agent_slug="claude",
|
|
458
|
+
base_ref=DEFAULT_BASE_REF,
|
|
459
|
+
maybe_worktree_root=str(configured_root),
|
|
460
|
+
)
|
|
461
|
+
worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
|
|
462
|
+
assert worktree_path == configured_root / "claude" / "fix" / "collision-2"
|
|
463
|
+
assert worktree_path.is_dir()
|
|
464
|
+
|
|
465
|
+
def should_reject_relative_worktree_root_without_creating_worktree(
|
|
466
|
+
self,
|
|
467
|
+
tmp_path: Path,
|
|
468
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
469
|
+
) -> None:
|
|
470
|
+
module = load_create_fresh_branch_module()
|
|
471
|
+
repository_path = build_repo_with_origin(tmp_path / "repo")
|
|
472
|
+
with pytest.raises(ValueError, match="absolute path") as raised_error:
|
|
473
|
+
module.create_fresh_branch(
|
|
474
|
+
branch_name="fix/relative-root",
|
|
475
|
+
repo_path=repository_path,
|
|
476
|
+
agent_slug="claude",
|
|
477
|
+
base_ref=DEFAULT_BASE_REF,
|
|
478
|
+
maybe_worktree_root="relative/worktrees",
|
|
479
|
+
)
|
|
480
|
+
assert str(raised_error.value) == ERROR_WORKTREE_ROOT_NOT_ABSOLUTE
|
|
481
|
+
assert not (repository_path / ".claude" / "worktrees").exists()
|
|
482
|
+
|
|
483
|
+
def should_reject_relative_worktree_root_before_fetch(
|
|
484
|
+
self,
|
|
485
|
+
tmp_path: Path,
|
|
486
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
487
|
+
) -> None:
|
|
488
|
+
module = load_create_fresh_branch_module()
|
|
489
|
+
repository_path = build_repo_with_origin(tmp_path / "repo")
|
|
490
|
+
all_fetch_calls: list[str] = []
|
|
491
|
+
|
|
492
|
+
def record_fetch(repo_root: Path, base_ref: str) -> None:
|
|
493
|
+
all_fetch_calls.append(base_ref)
|
|
494
|
+
raise AssertionError("fetch must not run for a relative worktree root")
|
|
495
|
+
|
|
496
|
+
monkeypatch.setattr(module, "fetch_base_ref", record_fetch)
|
|
497
|
+
with pytest.raises(ValueError, match="absolute path") as raised_error:
|
|
498
|
+
module.create_fresh_branch(
|
|
499
|
+
branch_name="fix/relative-root-before-fetch",
|
|
500
|
+
repo_path=repository_path,
|
|
501
|
+
agent_slug="claude",
|
|
502
|
+
base_ref=DEFAULT_BASE_REF,
|
|
503
|
+
maybe_worktree_root="relative/worktrees",
|
|
504
|
+
)
|
|
505
|
+
assert str(raised_error.value) == ERROR_WORKTREE_ROOT_NOT_ABSOLUTE
|
|
506
|
+
assert all_fetch_calls == []
|
|
507
|
+
|
|
508
|
+
def should_fetch_base_before_creating_worktree(
|
|
509
|
+
self,
|
|
510
|
+
tmp_path: Path,
|
|
511
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
512
|
+
) -> None:
|
|
513
|
+
module = load_create_fresh_branch_module()
|
|
514
|
+
repository_path = build_repo_with_origin(tmp_path / "repo")
|
|
515
|
+
all_call_names: list[str] = []
|
|
516
|
+
real_fetch = module.fetch_base_ref
|
|
517
|
+
real_create = module.create_worktree_branch
|
|
518
|
+
|
|
519
|
+
def record_fetch(
|
|
520
|
+
repo_root: Path,
|
|
521
|
+
base_ref: str,
|
|
522
|
+
) -> None:
|
|
523
|
+
all_call_names.append("fetch")
|
|
524
|
+
real_fetch(repo_root, base_ref)
|
|
525
|
+
|
|
526
|
+
def record_create(
|
|
527
|
+
repo_root: Path,
|
|
528
|
+
branch_name: str,
|
|
529
|
+
worktree_path: Path,
|
|
530
|
+
base_ref: str,
|
|
531
|
+
) -> None:
|
|
532
|
+
all_call_names.append("create")
|
|
533
|
+
real_create(
|
|
534
|
+
repo_root,
|
|
535
|
+
branch_name=branch_name,
|
|
536
|
+
worktree_path=worktree_path,
|
|
537
|
+
base_ref=base_ref,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
monkeypatch.setattr(module, "fetch_base_ref", record_fetch)
|
|
541
|
+
monkeypatch.setattr(module, "create_worktree_branch", record_create)
|
|
542
|
+
module.create_fresh_branch(
|
|
543
|
+
branch_name="fix/fetch-order",
|
|
544
|
+
repo_path=repository_path,
|
|
545
|
+
agent_slug="claude",
|
|
546
|
+
base_ref=DEFAULT_BASE_REF,
|
|
547
|
+
)
|
|
548
|
+
assert all_call_names == ["fetch", "create"]
|
|
549
|
+
|
|
550
|
+
|
|
361
551
|
class TestCreateFreshBranchIntegration:
|
|
362
552
|
def should_create_worktree_branch_without_moving_caller_head(
|
|
363
553
|
self,
|
|
@@ -670,3 +860,64 @@ class TestMainCli:
|
|
|
670
860
|
assert "relative path" in error_payload[PAYLOAD_KEY_ERROR]
|
|
671
861
|
assert not outside_target.exists()
|
|
672
862
|
assert not (tmp_path / "unused-repo").exists()
|
|
863
|
+
|
|
864
|
+
def should_print_error_json_for_relative_worktree_root(
|
|
865
|
+
self,
|
|
866
|
+
tmp_path: Path,
|
|
867
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
868
|
+
capsys: pytest.CaptureFixture[str],
|
|
869
|
+
) -> None:
|
|
870
|
+
module = load_create_fresh_branch_module()
|
|
871
|
+
repository_path = build_repo_with_origin(tmp_path / "repo")
|
|
872
|
+
monkeypatch.setattr(
|
|
873
|
+
sys,
|
|
874
|
+
"argv",
|
|
875
|
+
[
|
|
876
|
+
"create_fresh_branch.py",
|
|
877
|
+
"--branch-name",
|
|
878
|
+
"feat/relative-root",
|
|
879
|
+
"--repo",
|
|
880
|
+
str(repository_path),
|
|
881
|
+
"--agent",
|
|
882
|
+
"claude",
|
|
883
|
+
CLI_FLAG_WORKTREE_ROOT,
|
|
884
|
+
"relative/worktrees",
|
|
885
|
+
],
|
|
886
|
+
)
|
|
887
|
+
exit_code = module.main()
|
|
888
|
+
captured = capsys.readouterr()
|
|
889
|
+
assert exit_code == EXIT_CODE_FAILURE
|
|
890
|
+
error_payload = json.loads(captured.out)
|
|
891
|
+
assert error_payload[PAYLOAD_KEY_ERROR] == ERROR_WORKTREE_ROOT_NOT_ABSOLUTE
|
|
892
|
+
|
|
893
|
+
def should_print_success_json_for_absolute_worktree_root(
|
|
894
|
+
self,
|
|
895
|
+
tmp_path: Path,
|
|
896
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
897
|
+
capsys: pytest.CaptureFixture[str],
|
|
898
|
+
) -> None:
|
|
899
|
+
module = load_create_fresh_branch_module()
|
|
900
|
+
repository_path = build_repo_with_origin(tmp_path / "repo")
|
|
901
|
+
configured_root = (tmp_path / "cli-isolated").resolve()
|
|
902
|
+
monkeypatch.setattr(
|
|
903
|
+
sys,
|
|
904
|
+
"argv",
|
|
905
|
+
[
|
|
906
|
+
"create_fresh_branch.py",
|
|
907
|
+
"--branch-name",
|
|
908
|
+
"feat/cli-root",
|
|
909
|
+
"--repo",
|
|
910
|
+
str(repository_path),
|
|
911
|
+
"--agent",
|
|
912
|
+
"grok",
|
|
913
|
+
CLI_FLAG_WORKTREE_ROOT,
|
|
914
|
+
str(configured_root),
|
|
915
|
+
],
|
|
916
|
+
)
|
|
917
|
+
exit_code = module.main()
|
|
918
|
+
captured = capsys.readouterr()
|
|
919
|
+
assert exit_code == EXIT_CODE_SUCCESS
|
|
920
|
+
success_payload = json.loads(captured.out)
|
|
921
|
+
worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
|
|
922
|
+
assert worktree_path == configured_root / "grok" / "feat" / "cli-root"
|
|
923
|
+
assert worktree_path.is_dir()
|
|
@@ -148,6 +148,11 @@ Workers run with no turn cap. The timeout is the only bound on a worker's
|
|
|
148
148
|
length, and a worker that hits it is killed with its whole process tree and
|
|
149
149
|
reported as `timeout`.
|
|
150
150
|
|
|
151
|
+
A worker entry accepts these fields and no others. Any other key fails the
|
|
152
|
+
load with an error naming the stray key and listing the accepted set, so a
|
|
153
|
+
misspelled field shows up at startup rather than passing for a setting that
|
|
154
|
+
took effect.
|
|
155
|
+
|
|
151
156
|
Put the spec file under the run state directory (or any path you pass to
|
|
152
157
|
`--spec`).
|
|
153
158
|
|
|
@@ -226,3 +231,8 @@ Sibling skill: `/grokify` for a single paste-ready interactive Grok Build handof
|
|
|
226
231
|
|
|
227
232
|
- `SKILL.md` — hub and process.
|
|
228
233
|
- `reference/` — brief templates and flag profiles.
|
|
234
|
+
|
|
235
|
+
## Worker advisors
|
|
236
|
+
|
|
237
|
+
Each grok worker binds a unique Opus-high dvisor_session_id through the lead-supplied dvisor.launcher in the batch spec (committed default is a placeholder). Reports require the same handle\'s ENDORSE (or bounded CORRECTION/PLAN then ENDORSE); STOP or malformed signals end as advisor_blocked.
|
|
238
|
+
|
|
@@ -129,7 +129,10 @@ pending, or when the tool is `CronCreate`.
|
|
|
129
129
|
whole lifecycle (its Lifecycle ownership section); executors only ever
|
|
130
130
|
message the warm agent or report here, and an executor that finds the
|
|
131
131
|
advisor unreachable reports that upward — it never spawns a
|
|
132
|
-
replacement itself.
|
|
132
|
+
replacement itself. A **Fable**-tier attempt carries the exact token
|
|
133
|
+
`FABLE-SPAWN-AUTHORIZED` in its spawn prompt, as the protocol's
|
|
134
|
+
warm-up rule states; `hooks/blocking/fable_spawn_gate.py` denies a
|
|
135
|
+
fable spawn whose prompt lacks it.
|
|
133
136
|
3. **Write the run artifacts** (next section) before the first spawn.
|
|
134
137
|
4. **Activate status_gate** when the first open ledger task exists:
|
|
135
138
|
`python scripts/status_gate.py set --status active`.
|
|
@@ -355,3 +358,8 @@ no open work remains, run `set --status done` before any re-arm attempt.
|
|
|
355
358
|
- `scripts/` — deterministic status_gate.
|
|
356
359
|
- Advisor policy:
|
|
357
360
|
[`_shared/advisor/advisor-protocol.md`](../../_shared/advisor/advisor-protocol.md).
|
|
361
|
+
|
|
362
|
+
## File-backed run ledger
|
|
363
|
+
|
|
364
|
+
When host task tools are absent, reconcile delegated work through `scripts/grok_run_ledger.py` under the run-state directory (stable task ids, one live owner, unique advisor sessions, dependency blocking, snapshot-drift reopening).
|
|
365
|
+
|
|
@@ -89,7 +89,11 @@ once, and reports the unreachable advisor.
|
|
|
89
89
|
- **Claude host:** executors consult the warm `session-advisor` via
|
|
90
90
|
`SendMessage` (ENDORSE / CORRECTION / PLAN / STOP). This session
|
|
91
91
|
routes the same way; keep tool use to orchestration and light
|
|
92
|
-
verification reads.
|
|
92
|
+
verification reads. A drift re-spawn at the **Fable** tier carries
|
|
93
|
+
the exact token `FABLE-SPAWN-AUTHORIZED` in its fresh prompt, as
|
|
94
|
+
the protocol's warm-up rule states;
|
|
95
|
+
`hooks/blocking/fable_spawn_gate.py` denies a fable spawn whose
|
|
96
|
+
prompt lacks it.
|
|
93
97
|
- **Third-party host:** advisor is a max-tier Claude CLI bind owned
|
|
94
98
|
by this session (`claude_chain_runner.py`, Fable high then Opus
|
|
95
99
|
max). Do **not** spawn `session-advisor` via Agent. Executors
|
|
@@ -17,3 +17,6 @@ Do not invent a tool name. Tool availability means the callable tool is exposed
|
|
|
17
17
|
## Reporting
|
|
18
18
|
|
|
19
19
|
State the selected tool, the number of new items, the number already tracked, and the new task subjects. If the fallback path was used, name the unavailable preferred tools briefly.
|
|
20
|
+
|
|
21
|
+
4. **File ledger fallback** — when no host task/plan tool is exposed, register work in the durable `grok_run_ledger` under the run-state directory so orchestration stays reconcilable across hosts.
|
|
22
|
+
|