cowork-flow 1.9.2 → 2.1.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/docs/architecture.md +1 -34
  5. package/docs/direction.md +4 -3
  6. package/docs/hosts.md +14 -10
  7. package/docs/index.md +1 -1
  8. package/docs/release.md +1 -1
  9. package/package.json +6 -6
  10. package/presets/claude-code/.claude-plugin/plugin.json +1 -1
  11. package/presets/codex/.codex-plugin/plugin.json +1 -1
  12. package/presets/dsh/plugins/workflow-state.js +72 -24
  13. package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
  14. package/presets/kimi-code/hooks/cowork-flow-inject.mjs +28 -2
  15. package/presets/opencode/cowork-flow/plugin-core.js +70 -88
  16. package/presets/opencode/plugins/cowork-flow.js +7 -9
  17. package/presets/qoder/.qoder-plugin/plugin.json +1 -1
  18. package/presets/qoder/hooks/hooks.json +1 -1
  19. package/presets/qoder/hooks/inject-context.mjs +28 -2
  20. package/presets/zcode/.zcode-plugin/plugin.json +1 -1
  21. package/presets/zcode/hooks/inject-context.mjs +28 -3
  22. package/src/commands/host.js +23 -2
  23. package/src/commands/install-dsh-hook.js +1 -6
  24. package/src/commands/install-dsh-preset.js +1 -6
  25. package/src/commands/install-zcode-plugin.js +2 -3
  26. package/src/commands/registry.js +4 -2
  27. package/src/lib/asset-plan.js +1 -1
  28. package/src/lib/dsh-home.js +6 -0
  29. package/src/lib/plan-applier.js +1 -6
  30. package/template/.claude/agents/cowork-check.md +1 -1
  31. package/template/.claude/agents/cowork-implement.md +1 -1
  32. package/template/.claude/agents/cowork-research.md +1 -1
  33. package/template/.codex/agents/cowork-check.toml +1 -1
  34. package/template/.codex/agents/cowork-implement.toml +1 -1
  35. package/template/.codex/agents/cowork-research.toml +1 -1
  36. package/template/.codex/agents/default.toml +1 -1
  37. package/template/.codex/agents/explorer.toml +1 -1
  38. package/template/.codex/agents/worker.toml +1 -1
  39. package/template/.cowork-flow/.version +1 -1
  40. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +1 -1
  41. package/template/.cowork-flow/adapters/codex/adapter.yaml +1 -1
  42. package/template/.cowork-flow/adapters/dsh/adapter.yaml +1 -1
  43. package/template/.cowork-flow/adapters/opencode/adapter.yaml +1 -1
  44. package/template/.cowork-flow/adapters/qoder/adapter.yaml +1 -1
  45. package/template/.cowork-flow/run +2 -2
  46. package/template/.cowork-flow/run.cmd +3 -3
  47. package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +0 -1
  48. package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +0 -7
  49. package/template/.cowork-flow/scripts/adapters/host/claude_code_policy.py +6 -4
  50. package/template/.cowork-flow/scripts/adapters/host/codex_policy.py +8 -6
  51. package/template/.cowork-flow/scripts/adapters/host/edit_scope.py +113 -0
  52. package/template/.cowork-flow/scripts/adapters/host/opencode_policy.py +12 -0
  53. package/template/.cowork-flow/scripts/adapters/host/qoder_policy.py +7 -5
  54. package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +0 -91
  55. package/template/.cowork-flow/scripts/adapters/host/zcode_policy.py +15 -77
  56. package/template/.cowork-flow/scripts/run.py +0 -2
  57. package/template/.cowork-flow/scripts/services/context_paths.py +2 -2
  58. package/template/.cowork-flow/scripts/services/fact_view.py +49 -21
  59. package/template/.cowork-flow/scripts/services/lifecycle_checks.py +1 -216
  60. package/template/.cowork-flow/scripts/services/lifecycle_policy.py +0 -5
  61. package/template/.cowork-flow/scripts/services/task_gate_profiles.py +1 -1
  62. package/template/.cowork-flow/scripts/services/task_lifecycle.py +14 -31
  63. package/template/.cowork-flow/spec/backend/error-handling.md +0 -14
  64. package/template/.cowork-flow/spec/contracts/context-injection.md +53 -26
  65. package/template/.cowork-flow/spec/contracts/decision-anchor.md +3 -3
  66. package/template/.cowork-flow/spec/contracts/error-codes.md +1 -3
  67. package/template/.cowork-flow/spec/contracts/evidence-completion.md +6 -8
  68. package/template/.cowork-flow/spec/contracts/index.md +0 -1
  69. package/template/.cowork-flow/spec/contracts/plan-binding.md +3 -2
  70. package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +6 -21
  71. package/template/.cowork-flow/spec/contracts/task-adopt.md +1 -1
  72. package/template/.cowork-flow/spec/runtime/contract-registry.json +0 -13
  73. package/template/.cowork-flow/spec/runtime/host-assets.json +10 -7
  74. package/template/.cowork-flow/spec/runtime/scope-rules.json +2 -2
  75. package/template/.kimi-code/agents/cowork-check.md +1 -1
  76. package/template/.kimi-code/agents/cowork-implement.md +1 -1
  77. package/template/.kimi-code/agents/cowork-research.md +1 -1
  78. package/template/.opencode/agents/cowork-check.md +1 -1
  79. package/template/.opencode/agents/cowork-implement.md +1 -1
  80. package/template/.opencode/agents/cowork-research.md +1 -1
  81. package/template/.opencode/cowork-flow/plugin-core.js +70 -88
  82. package/template/.opencode/plugins/cowork-flow.js +7 -9
  83. package/template/AGENTS.md +0 -1
  84. package/template/skills/agent-dispatch/SKILL.md +0 -15
  85. package/template/skills/runtime-health/scripts/doctor.py +83 -58
  86. package/template/skills/spec-sync/SKILL.md +0 -1
  87. package/template/skills/task-planning/SKILL.md +1 -1
  88. package/template/skills/task-review/SKILL.md +3 -3
  89. package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +0 -178
  90. package/template/.cowork-flow/scripts/services/spec_check.py +0 -573
  91. package/template/.cowork-flow/spec/contracts/spec-checks.md +0 -281
@@ -13,7 +13,7 @@ capabilities:
13
13
  runtimeContextDispatch: native
14
14
  runtimeContextBinding: shim
15
15
  runtimeContextCleanup: shim
16
- editScopeWarning: unsupported
16
+ editScopeWarning: plugin
17
17
  dispatch:
18
18
  primitive: subagent
19
19
  memoryPath: AGENTS.md
@@ -13,7 +13,7 @@ capabilities:
13
13
  runtimeContextDispatch: native
14
14
  runtimeContextBinding: shim
15
15
  runtimeContextCleanup: native
16
- editScopeWarning: unsupported
16
+ editScopeWarning: plugin
17
17
  dispatch:
18
18
  primitive: task
19
19
  agentsPath: .opencode/agents
@@ -13,7 +13,7 @@ capabilities:
13
13
  runtimeContextDispatch: native
14
14
  runtimeContextBinding: shim
15
15
  runtimeContextCleanup: native
16
- editScopeWarning: unsupported
16
+ editScopeWarning: plugin
17
17
  dispatch:
18
18
  primitive: subagent
19
19
  # Host assets ship as a machine-level plugin; their layout is owned by
@@ -1,8 +1,8 @@
1
1
  #!/bin/sh
2
2
  set -u
3
3
 
4
- MIN_PYTHON_LABEL="Python 3.9+"
5
- VERSION_CHECK='import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)'
4
+ MIN_PYTHON_LABEL="Python 3.10+"
5
+ VERSION_CHECK='import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)'
6
6
  WORKFLOW_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
7
7
  RUNNER_SCRIPT="$WORKFLOW_DIR/scripts/run.py"
8
8
 
@@ -1,8 +1,8 @@
1
1
  @echo off
2
2
  setlocal EnableExtensions
3
3
 
4
- set "MIN_PYTHON_LABEL=Python 3.9+"
5
- set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)"
4
+ set "MIN_PYTHON_LABEL=Python 3.10+"
5
+ set "VERSION_CHECK=import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)"
6
6
  set "WORKFLOW_DIR=%~dp0"
7
7
  set "RUNNER_SCRIPT=%WORKFLOW_DIR%scripts\run.py"
8
8
  set "SELECTED_PYTHON="
@@ -46,7 +46,7 @@ if not errorlevel 1 (
46
46
  )
47
47
 
48
48
  echo Error: %ENV_NAME% does not point to a usable %MIN_PYTHON_LABEL% interpreter: %ENV_VALUE% 1>&2
49
- echo Set %ENV_NAME% to an executable Python 3.9+ interpreter path. 1>&2
49
+ echo Set %ENV_NAME% to an executable Python 3.10+ interpreter path. 1>&2
50
50
  exit /b 127
51
51
 
52
52
  :select_python
@@ -441,7 +441,6 @@ def cmd_complete(args: argparse.Namespace) -> int:
441
441
  result = service.complete(
442
442
  task_dir,
443
443
  execution_context=execution_context,
444
- allow_unchecked_specs=bool(getattr(args, "allow_unchecked", False)),
445
444
  allow_missing_evidence=bool(
446
445
  getattr(args, "allow_missing_evidence", False)
447
446
  ),
@@ -95,13 +95,6 @@ def _add_next_output_options(parser: argparse.ArgumentParser) -> None:
95
95
  action="store_true",
96
96
  help="Validate task context JSONL without changing task state",
97
97
  )
98
- parser.add_argument(
99
- "--allow-unchecked",
100
- dest="allow_unchecked",
101
- action="store_true",
102
- help="With task completion: allow unchecked spec-check commands "
103
- "(missing/timeout) and record the exemption in task.json meta",
104
- )
105
98
  parser.add_argument(
106
99
  "--allow-missing-evidence",
107
100
  dest="allow_missing_evidence",
@@ -4,8 +4,7 @@ workflow hook.
4
4
 
5
5
  Rendering stays in workflow_state_hook.py; this module owns only the
6
6
  claude-code transport specifics — the runtime preamble block and the
7
- PostToolUse spec-advisory path (stderr + exit 2, shared with codex via the
8
- neutral helper).
7
+ PostToolUse edit-scope advisory.
9
8
  """
10
9
 
11
10
  from __future__ import annotations
@@ -13,8 +12,8 @@ from __future__ import annotations
13
12
  from pathlib import Path
14
13
  from typing import Any
15
14
 
15
+ from adapters.host.edit_scope import edit_scope_warning
16
16
  from adapters.host.policy_base import HostPolicy
17
- from adapters.host.workflow_state_hook import spec_only_post_tool_use
18
17
 
19
18
  CLAUDE_PREAMBLE = (
20
19
  (
@@ -30,7 +29,10 @@ def preamble(root: Path) -> tuple[str, ...]:
30
29
 
31
30
 
32
31
  def post_tool_use(root: Path, hook_input: dict[str, Any]) -> tuple[str, int]:
33
- return spec_only_post_tool_use(root, hook_input, POLICY)
32
+ """PostToolUse transport: the edit-scope advisory rides the standard
33
+ additionalContext envelope, exit 0 — the edit already happened, so
34
+ this decorates the result and never blocks it."""
35
+ return edit_scope_warning(root, hook_input), 0
34
36
 
35
37
 
36
38
  POLICY = HostPolicy(
@@ -3,20 +3,19 @@
3
3
 
4
4
  Rendering stays in workflow_state_hook.py; this module owns only the codex
5
5
  transport specifics — the dispatch-mode preamble (config-driven, the codex
6
- wrapper surfaces it above every injection) and the probe-based session-start
6
+ wrapper surfaces it above every injection), the probe-based session-start
7
7
  detection (codex registers UserPromptSubmit only, so the digest shape derives
8
- from the session state file probe). PostToolUse reuses the neutral spec-only
9
- stderr path shared with claude-code.
8
+ from the session state file probe), and the PostToolUse edit-scope advisory.
10
9
  """
11
10
 
12
11
  from __future__ import annotations
13
12
 
14
- import sys
15
13
  from pathlib import Path
16
14
  from typing import Any
17
15
 
16
+ from adapters.host.edit_scope import edit_scope_warning
18
17
  from adapters.host.policy_base import HostPolicy
19
- from adapters.host.workflow_state_hook import _load_common, spec_only_post_tool_use
18
+ from adapters.host.workflow_state_hook import _load_common
20
19
 
21
20
 
22
21
  def codex_dispatch_mode(root: Path) -> str:
@@ -47,7 +46,10 @@ def preamble(root: Path) -> tuple[str, ...]:
47
46
 
48
47
 
49
48
  def post_tool_use(root: Path, hook_input: dict[str, Any]) -> tuple[str, int]:
50
- return spec_only_post_tool_use(root, hook_input, POLICY)
49
+ """PostToolUse transport: the edit-scope advisory rides the standard
50
+ additionalContext envelope, exit 0. apply_patch payloads carry their
51
+ paths inside the patch text, which the shared extractor covers."""
52
+ return edit_scope_warning(root, hook_input), 0
51
53
 
52
54
 
53
55
  POLICY = HostPolicy(
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """Shared edit-phase scope advisory (the PostToolUse short path).
3
+
4
+ At most one line when an edited file is outside the active task's
5
+ file-scope whitelist; empty string (silent) when in scope, when the task
6
+ is not in a spec-contract state, for subagent sessions, or on any error.
7
+ Hosts differ only in transport: inject.py hosts reach this through their
8
+ policy `post_tool_use` slot, the dsh preset plugin through its own Python
9
+ protocol. The wording is frozen by context-injection.md — never fork it
10
+ per host.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from adapters.host.workflow_state_hook import (
20
+ STAGE_CONTRACT_STATES,
21
+ _get_active_task_with_fallback,
22
+ _session_scope,
23
+ )
24
+
25
+ _APPLY_PATCH_FILE_RE = re.compile(
26
+ r"^\*\*\* (?:Update|Add|Delete) File: (.+?)\s*$", re.MULTILINE
27
+ )
28
+
29
+
30
+ def extract_edited_paths(tool_input: Any) -> list[str]:
31
+ """Best-effort edited file paths from a PostToolUse tool_input payload.
32
+
33
+ Hosts differ: claude-code, zcode, qoder and opencode pass file_path,
34
+ while codex's apply_patch carries the paths inside the patch text.
35
+ Returning every extractable path keeps a multi-file patch from
36
+ silently skipping the warning; deduplication keeps one edit from
37
+ checking the same file twice."""
38
+ if not isinstance(tool_input, dict):
39
+ return []
40
+ paths: list[str] = []
41
+ for key in ("file_path", "filePath", "path"):
42
+ value = tool_input.get(key)
43
+ if isinstance(value, str) and value.strip():
44
+ paths.append(value.strip())
45
+ for key in ("input", "patch", "patchText"):
46
+ text = tool_input.get(key)
47
+ if isinstance(text, str):
48
+ paths.extend(
49
+ match.strip() for match in _APPLY_PATCH_FILE_RE.findall(text)
50
+ )
51
+ seen: set[str] = set()
52
+ unique: list[str] = []
53
+ for path in paths:
54
+ if path not in seen:
55
+ seen.add(path)
56
+ unique.append(path)
57
+ return unique
58
+
59
+
60
+ def edit_scope_warning(
61
+ root: Path,
62
+ hook_input: dict[str, Any],
63
+ *,
64
+ fallback_for_unbound: bool = False,
65
+ ) -> str:
66
+ """At most one line when the edited file is outside the task's
67
+ file-scope whitelist. Silence rules: no active in_progress/review
68
+ task, a subagent session, an in-scope path, an unreadable whitelist,
69
+ or no extractable path. `fallback_for_unbound` follows the calling
70
+ host's policy (zcode: true). Never raises — the edit has already
71
+ happened."""
72
+ try:
73
+ task_path, status, _source = _get_active_task_with_fallback(
74
+ root, hook_input, fallback_for_unbound
75
+ )
76
+ except Exception:
77
+ return ""
78
+ if not task_path or status not in STAGE_CONTRACT_STATES:
79
+ return ""
80
+ if _session_scope(root, hook_input) == "subagent":
81
+ return ""
82
+ paths = extract_edited_paths(hook_input.get("tool_input"))
83
+ if not paths:
84
+ return ""
85
+ try:
86
+ from services.fact_view import file_scope_whitelist, path_in_scope
87
+
88
+ whitelist = file_scope_whitelist(root, root / task_path)
89
+ except Exception:
90
+ return ""
91
+ for file_path in paths:
92
+ # Hosts pass absolute edit paths; whitelist entries are
93
+ # repo-relative. Same normalization as run_edit_checks took.
94
+ normalized = file_path.replace("\\", "/").strip()
95
+ try:
96
+ normalized = (
97
+ Path(normalized).resolve().relative_to(root.resolve()).as_posix()
98
+ )
99
+ except (ValueError, OSError):
100
+ pass
101
+ try:
102
+ if path_in_scope(whitelist, normalized).get("inScope"):
103
+ continue
104
+ except Exception:
105
+ return ""
106
+ while normalized.startswith("./"):
107
+ normalized = normalized[2:]
108
+ return (
109
+ f"⚠️ {normalized} is outside the task's declared scope. "
110
+ "If intended, declare it in the task's `implement.jsonl` (agent-mutable) "
111
+ "or revert the edit."
112
+ )
113
+ return ""
@@ -12,6 +12,10 @@ session file probe (session_start_event=None), as with codex.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from adapters.host.edit_scope import edit_scope_warning
15
19
  from adapters.host.policy_base import HostPolicy
16
20
 
17
21
  OPENCODE_DIGEST_POLICY = (
@@ -20,6 +24,13 @@ OPENCODE_DIGEST_POLICY = (
20
24
  )
21
25
 
22
26
 
27
+ def post_tool_use(root: Path, hook_input: dict[str, Any]) -> tuple[str, int]:
28
+ """PostToolUse transport for the plugin's tool.execute.after hook: the
29
+ edit-scope advisory is bare text (emit_text), appended to the finished
30
+ tool output by the shim. Exit 0 — never blocks the edit."""
31
+ return edit_scope_warning(root, hook_input), 0
32
+
33
+
23
34
  POLICY = HostPolicy(
24
35
  host="opencode",
25
36
  digest_policy=OPENCODE_DIGEST_POLICY,
@@ -29,4 +40,5 @@ POLICY = HostPolicy(
29
40
  emit_not_initialized=True,
30
41
  # stdout is the injection itself: the shim forwards it verbatim.
31
42
  emit_text=True,
43
+ post_tool_use=post_tool_use,
32
44
  )
@@ -3,9 +3,7 @@
3
3
 
4
4
  Rendering stays in workflow_state_hook.py; this module owns only what Qoder
5
5
  changes. Qoder shares the JSON envelope shape (`hookSpecificOutput`) with
6
- claude-code and codex, but its `PostToolUse` is not a blockable event: exit 2
7
- there is ignored, so the edit-phase spec advisory must travel as
8
- `additionalContext` on exit 0 instead of stderr.
6
+ claude-code and codex.
9
7
 
10
8
  Host assets (hooks declaration, fixed subagents, commands, skills) ship as a
11
9
  machine-level Qoder plugin, so the hook reaches this entry through the plugin
@@ -17,8 +15,9 @@ from __future__ import annotations
17
15
  from pathlib import Path
18
16
  from typing import Any
19
17
 
18
+ from adapters.host.edit_scope import edit_scope_warning
20
19
  from adapters.host.policy_base import HostPolicy
21
- from adapters.host.workflow_state_hook import hook_state_summary, spec_edit_warning
20
+ from adapters.host.workflow_state_hook import hook_state_summary
22
21
 
23
22
  QODER_PREAMBLE = (
24
23
  (
@@ -34,7 +33,10 @@ def preamble(root: Path) -> tuple[str, ...]:
34
33
 
35
34
 
36
35
  def post_tool_use(root: Path, hook_input: dict[str, Any]) -> tuple[str, int]:
37
- return spec_edit_warning(root, hook_input, POLICY), 0
36
+ """PostToolUse transport: the edit-scope advisory rides the standard
37
+ additionalContext envelope, exit 0 (never a systemMessage — that gate
38
+ stays on the two injecting events)."""
39
+ return edit_scope_warning(root, hook_input), 0
38
40
 
39
41
 
40
42
  # The task directory name is user-chosen and unbounded, and the host renders
@@ -466,97 +466,6 @@ def _stage_contract_block(
466
466
  return None
467
467
 
468
468
 
469
- _APPLY_PATCH_FILE_RE = re.compile(
470
- r"^\*\*\* (?:Update|Add|Delete) File: (.+?)\s*$", re.MULTILINE
471
- )
472
-
473
-
474
- def _extract_edited_paths(tool_input: Any) -> list[str]:
475
- """Best-effort edited file paths from a PostToolUse tool_input payload.
476
-
477
- Hosts differ: claude-code and zcode pass file_path, while codex's
478
- apply_patch carries the paths inside the patch text. Returning every
479
- extractable path keeps a multi-file patch from silently skipping
480
- checks; deduplication keeps one edit from burning the throttle window
481
- on the same file twice."""
482
- if not isinstance(tool_input, dict):
483
- return []
484
- paths: list[str] = []
485
- for key in ("file_path", "filePath", "path"):
486
- value = tool_input.get(key)
487
- if isinstance(value, str) and value.strip():
488
- paths.append(value.strip())
489
- for key in ("input", "patch", "patchText"):
490
- text = tool_input.get(key)
491
- if isinstance(text, str):
492
- paths.extend(
493
- match.strip() for match in _APPLY_PATCH_FILE_RE.findall(text)
494
- )
495
- seen: set[str] = set()
496
- unique: list[str] = []
497
- for path in paths:
498
- if path not in seen:
499
- seen.add(path)
500
- unique.append(path)
501
- return unique
502
-
503
-
504
- def spec_edit_warning(
505
- root: Path,
506
- hook_input: dict[str, Any],
507
- policy: HostPolicy,
508
- ) -> str:
509
- """Editor-phase spec-check single-line warning (PostToolUse short path).
510
-
511
- Delegated subagents are the primary coders, so spec violations must
512
- reach them too: only an active in_progress/review task is required —
513
- session scope is not consulted here (scope warnings stay main-only in
514
- the zcode policy's edit_scope_warning). Hosts with
515
- fallback_for_unbound (zcode) follow another main session's binding for
516
- their never-bound hook sessions only while exactly one main-session
517
- binding exists (see _get_active_task_with_fallback); strict-identity
518
- hosts (claude-code/codex) require the calling session's own binding.
519
- no_task, planning, and completed stay silent. Empty string means
520
- silent. Never raises — the editor path must not break edits.
521
- """
522
- try:
523
- task_path, status, _source = _get_active_task_with_fallback(
524
- root, hook_input, policy.fallback_for_unbound
525
- )
526
- except Exception:
527
- return ""
528
- if not task_path or status not in STAGE_CONTRACT_STATES:
529
- return ""
530
- paths = _extract_edited_paths(hook_input.get("tool_input"))
531
- if not paths:
532
- return ""
533
- try:
534
- from services.spec_check import run_edit_checks
535
-
536
- for path in paths:
537
- warning = run_edit_checks(root, path)
538
- if warning:
539
- return warning
540
- return ""
541
- except Exception:
542
- return ""
543
-
544
-
545
- def spec_only_post_tool_use(
546
- root: Path,
547
- hook_input: dict[str, Any],
548
- policy: HostPolicy,
549
- ) -> tuple[str, int]:
550
- """claude-code / codex PostToolUse transport: spec-check advisory on
551
- stderr only (exit 2 surfaces it to the model); the edit itself has
552
- already happened. No additionalContext payload."""
553
- warning = spec_edit_warning(root, hook_input, policy)
554
- if warning:
555
- print(warning, file=sys.stderr)
556
- return "", 2
557
- return "", 0
558
-
559
-
560
469
  def _load_breadcrumbs(root: Path) -> dict[str, str]:
561
470
  path = (
562
471
  root
@@ -4,27 +4,20 @@
4
4
  Rendering stays in workflow_state_hook.py (single source shared with the
5
5
  other hosts); this module owns only what is zcode-specific — the digest
6
6
  policy wording, the newest-session fallback for unbound hook sessions, the
7
- rebind hints and essential-files check ported from the zcode JS hook, the
8
- merged scope+spec edit warning, and the PostToolUse transport contract the
9
- zcode shim relies on.
7
+ rebind hints and essential-files check ported from the zcode JS hook, and
8
+ the PostToolUse transport contract the zcode shim relies on.
10
9
  """
11
10
 
12
11
  from __future__ import annotations
13
12
 
14
13
  import json
15
14
  import re
16
- import sys
17
15
  from pathlib import Path
18
16
  from typing import Any
19
17
 
18
+ from adapters.host.edit_scope import edit_scope_warning
20
19
  from adapters.host.policy_base import HostPolicy
21
- from adapters.host.workflow_state_hook import (
22
- STAGE_CONTRACT_STATES,
23
- _get_active_task_with_fallback,
24
- _session_scope,
25
- build_hook_context,
26
- spec_edit_warning,
27
- )
20
+ from adapters.host.workflow_state_hook import build_hook_context
28
21
 
29
22
  # Wording frozen by context-injection.md: zcode drops the
30
23
  # registry warning and asks for the fingerprint every hook.
@@ -162,72 +155,12 @@ def essential_files_warning(root: Path) -> str:
162
155
  )
163
156
 
164
157
 
165
- def edit_scope_warning(root: Path, hook_input: dict[str, Any]) -> str:
166
- """Per-edit out-of-scope warning (zcode-only capability). Port of the
167
- zcode hook's editScopeWarning: at most one line when the edited file is
168
- outside the task's file-scope whitelist. Silence rules match the JS
169
- source, including the newest-session display fallback for sessions with
170
- no resolvable identity. Never raises."""
171
- try:
172
- task_path, status, _source = _get_active_task_with_fallback(
173
- root, hook_input, POLICY.fallback_for_unbound
174
- )
175
- except Exception:
176
- return ""
177
- if not task_path or status not in STAGE_CONTRACT_STATES:
178
- return ""
179
- if _session_scope(root, hook_input) == "subagent":
180
- return ""
181
- tool_input = hook_input.get("tool_input")
182
- file_path = (
183
- tool_input.get("file_path") if isinstance(tool_input, dict) else None
184
- )
185
- if not isinstance(file_path, str) or not file_path.strip():
186
- return ""
187
- # Hosts pass absolute edit paths; whitelist entries are
188
- # repo-relative. Same normalization as run_edit_checks.
189
- normalized = file_path.replace("\\", "/").strip()
190
- try:
191
- normalized = (
192
- Path(normalized).resolve().relative_to(root.resolve()).as_posix()
193
- )
194
- except (ValueError, OSError):
195
- pass
196
- try:
197
- from services.fact_view import file_scope_whitelist, path_in_scope
198
-
199
- whitelist = file_scope_whitelist(root, root / task_path)
200
- if path_in_scope(whitelist, normalized).get("inScope"):
201
- return ""
202
- except Exception:
203
- return ""
204
- while normalized.startswith("./"):
205
- normalized = normalized[2:]
206
- return (
207
- f"⚠️ {normalized} is outside the task's declared scope. "
208
- "If intended, add it with `task context add` (agent-mutable) "
209
- "or revert the edit."
210
- )
211
-
212
-
213
- def merged_edit_warning(root: Path, hook_input: dict[str, Any]) -> str:
214
- """zcode PostToolUse Edit/Write/MultiEdit context: the scope warning and
215
- the spec-check warning merged into one additionalContext payload, at
216
- most one line each, joined by a newline (port of the zcode hook's
217
- mergedEditWarning)."""
218
- scope_line = edit_scope_warning(root, hook_input)
219
- spec_line = spec_edit_warning(root, hook_input, POLICY)
220
- if scope_line and spec_line:
221
- return f"{scope_line}\n{spec_line}"
222
- return scope_line or spec_line
223
-
224
-
225
158
  def post_tool_use(root: Path, hook_input: dict[str, Any]) -> tuple[str, int]:
226
- """zcode PostToolUse transport: Edit/Write/MultiEdit get the merged
227
- edit warning; Bash events refresh the full state only for workflow
228
- lifecycle commands (the shim pre-filters the common case, this keeps
229
- the entry correct standalone). Edit storms must not multiply the
230
- injection payload."""
159
+ """zcode PostToolUse transport: Edit/Write/MultiEdit get the shared
160
+ scope warning (with zcode's newest-session fallback); Bash events
161
+ refresh the full state only for workflow lifecycle commands (the shim
162
+ pre-filters the common case, this keeps the entry correct standalone).
163
+ Edit storms must not multiply the injection payload."""
231
164
  tool_name = str(hook_input.get("tool_name") or "")
232
165
  tool_input = hook_input.get("tool_input")
233
166
  file_path = (
@@ -238,7 +171,12 @@ def post_tool_use(root: Path, hook_input: dict[str, Any]) -> tuple[str, int]:
238
171
  and isinstance(file_path, str)
239
172
  and file_path.strip()
240
173
  ):
241
- return merged_edit_warning(root, hook_input), 0
174
+ return (
175
+ edit_scope_warning(
176
+ root, hook_input, fallback_for_unbound=POLICY.fallback_for_unbound
177
+ ),
178
+ 0,
179
+ )
242
180
  if not is_lifecycle_bash(hook_input):
243
181
  return "", 0
244
182
  claim_after_lifecycle_bash(root, hook_input)
@@ -32,7 +32,6 @@ COMMAND_SCRIPTS = {
32
32
  "state": "adapters/cli/state.py",
33
33
  "mcp-state": "adapters/mcp/state_server.py",
34
34
  "subagent": "adapters/cli/subagent.py",
35
- "spec-check": "adapters/cli/spec_check.py",
36
35
  }
37
36
  RESERVED_COMMAND_NAMES = (*COMMAND_SCRIPTS, "python", "help", "-h", "--help")
38
37
 
@@ -51,7 +50,6 @@ Common commands:
51
50
  resume
52
51
  task
53
52
  state
54
- spec-check
55
53
  get-context
56
54
  get-developer
57
55
  init-developer
@@ -24,10 +24,10 @@ DEFAULT_SCOPE_RULES: dict = {
24
24
  "trailingSlashRejectedTypes": ["planned-file", "deleted-file"],
25
25
  },
26
26
  "stageContract": {
27
- "budget": 1200,
27
+ "budget": 3400,
28
28
  "scopeLimit": 8,
29
29
  "specLimit": 4,
30
- "verifyLimit": 3,
30
+ "verifyLimit": 10,
31
31
  },
32
32
  }
33
33