forge-cw 0.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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/console/assets/addon-fit-BlxrFPDK.js +1 -0
  3. package/console/assets/addon-web-links-BWjmmSgS.js +12 -0
  4. package/console/assets/index-Digctq9W.css +32 -0
  5. package/console/assets/index-ItVUTuQf.js +31 -0
  6. package/console/assets/xterm-DlVFs1Kw.js +9 -0
  7. package/console/index.html +13 -0
  8. package/cw/.bundle.json +4 -0
  9. package/cw/agents/presentation-curator.md +119 -0
  10. package/cw/agents/weather-agent.md +67 -0
  11. package/cw/cw +6188 -0
  12. package/cw/cw-shell-integration.sh +132 -0
  13. package/cw/hooks/HOOKS-README.md +471 -0
  14. package/cw/hooks/config/hooks-config.json +24 -0
  15. package/cw/hooks/scripts/auto-compact.py +133 -0
  16. package/cw/hooks/scripts/hooks.py +472 -0
  17. package/cw/hooks/scripts/review-autoclose.py +171 -0
  18. package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.mp3 +0 -0
  19. package/cw/hooks/sounds/agent_permissionrequest/agent_permissionrequest.wav +0 -0
  20. package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.mp3 +0 -0
  21. package/cw/hooks/sounds/agent_posttooluse/agent_posttooluse.wav +0 -0
  22. package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.mp3 +0 -0
  23. package/cw/hooks/sounds/agent_posttoolusefailure/agent_posttoolusefailure.wav +0 -0
  24. package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.mp3 +0 -0
  25. package/cw/hooks/sounds/agent_pretooluse/agent_pretooluse.wav +0 -0
  26. package/cw/hooks/sounds/agent_stop/agent_stop.mp3 +0 -0
  27. package/cw/hooks/sounds/agent_stop/agent_stop.wav +0 -0
  28. package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.mp3 +0 -0
  29. package/cw/hooks/sounds/agent_subagentstop/agent_subagentstop.wav +0 -0
  30. package/cw/hooks/sounds/configchange/configchange.mp3 +0 -0
  31. package/cw/hooks/sounds/configchange/configchange.wav +0 -0
  32. package/cw/hooks/sounds/notification/notification.mp3 +0 -0
  33. package/cw/hooks/sounds/notification/notification.wav +0 -0
  34. package/cw/hooks/sounds/permissionrequest/permissionrequest.mp3 +0 -0
  35. package/cw/hooks/sounds/permissionrequest/permissionrequest.wav +0 -0
  36. package/cw/hooks/sounds/posttooluse/posttooluse.mp3 +0 -0
  37. package/cw/hooks/sounds/posttooluse/posttooluse.wav +0 -0
  38. package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.mp3 +0 -0
  39. package/cw/hooks/sounds/posttoolusefailure/posttoolusefailure.wav +0 -0
  40. package/cw/hooks/sounds/precompact/precompact.mp3 +0 -0
  41. package/cw/hooks/sounds/precompact/precompact.wav +0 -0
  42. package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.mp3 +0 -0
  43. package/cw/hooks/sounds/pretooluse/pretooluse-git-committing.wav +0 -0
  44. package/cw/hooks/sounds/pretooluse/pretooluse.mp3 +0 -0
  45. package/cw/hooks/sounds/pretooluse/pretooluse.wav +0 -0
  46. package/cw/hooks/sounds/sessionend/sessionend.mp3 +0 -0
  47. package/cw/hooks/sounds/sessionend/sessionend.wav +0 -0
  48. package/cw/hooks/sounds/sessionstart/sessionstart.mp3 +0 -0
  49. package/cw/hooks/sounds/sessionstart/sessionstart.wav +0 -0
  50. package/cw/hooks/sounds/setup/Setup.mp3 +0 -0
  51. package/cw/hooks/sounds/setup/Setup.wav +0 -0
  52. package/cw/hooks/sounds/stop/stop.mp3 +0 -0
  53. package/cw/hooks/sounds/stop/stop.wav +0 -0
  54. package/cw/hooks/sounds/subagentstart/subagentstart.mp3 +0 -0
  55. package/cw/hooks/sounds/subagentstart/subagentstart.wav +0 -0
  56. package/cw/hooks/sounds/subagentstop/subagentstop.mp3 +0 -0
  57. package/cw/hooks/sounds/subagentstop/subagentstop.wav +0 -0
  58. package/cw/hooks/sounds/taskcompleted/taskcompleted.mp3 +0 -0
  59. package/cw/hooks/sounds/taskcompleted/taskcompleted.wav +0 -0
  60. package/cw/hooks/sounds/teammateidle/teammateidle.mp3 +0 -0
  61. package/cw/hooks/sounds/teammateidle/teammateidle.wav +0 -0
  62. package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.mp3 +0 -0
  63. package/cw/hooks/sounds/userpromptsubmit/userpromptsubmit.wav +0 -0
  64. package/cw/hooks/sounds/worktreecreate/worktreecreate.mp3 +0 -0
  65. package/cw/hooks/sounds/worktreecreate/worktreecreate.wav +0 -0
  66. package/cw/hooks/sounds/worktreeremove/worktreeremove.mp3 +0 -0
  67. package/cw/hooks/sounds/worktreeremove/worktreeremove.wav +0 -0
  68. package/cw/install.sh +243 -0
  69. package/cw/lib/context/github.sh +51 -0
  70. package/cw/lib/context/linear.sh +49 -0
  71. package/cw/lib/context/notion.sh +38 -0
  72. package/cw/lib/dashboard/activity-hook.py +134 -0
  73. package/cw/lib/dashboard/index.html +1515 -0
  74. package/cw/lib/dashboard/server.py +379 -0
  75. package/cw/lib/dashboard/start.sh +15 -0
  76. package/cw/lib/harnesses/claude.sh +92 -0
  77. package/cw/lib/harnesses/codex.sh +245 -0
  78. package/cw/lib/harnesses/opencode.sh +120 -0
  79. package/cw/lib/harnesses/pi.sh +66 -0
  80. package/cw/lib/iterm2.sh +140 -0
  81. package/cw/templates/CLAUDE.template.md +48 -0
  82. package/dist/index.js +4376 -0
  83. package/package.json +51 -0
@@ -0,0 +1,245 @@
1
+ # codex cli driver
2
+ # unverified: no skip-permissions equivalent flag confirmed, so it stays unsupported
3
+ codex_supports() {
4
+ local cap
5
+ for cap in continue_last non_interactive_prompt model_flag custom_provider \
6
+ instructions_file api_key_login headless_login; do
7
+ [[ "$1" == "$cap" ]] && return 0
8
+ done
9
+ return 1
10
+ }
11
+
12
+ codex_config_env() {
13
+ printf 'CODEX_HOME=%s\n' "$CW_HARNESS_DIR"
14
+ }
15
+ # unverified: no model_providers table is written for a custom endpoint, only the provider name
16
+
17
+ # lists the rollout files codex has written for this account
18
+ _codex_rollouts() {
19
+ [[ -d "$CW_HARNESS_DIR/sessions" ]] || return 0
20
+ find "$CW_HARNESS_DIR/sessions" -type f -name 'rollout-*.jsonl' 2>/dev/null | sort
21
+ }
22
+
23
+ # remembers which rollouts existed before a launch, so the new one can be told apart
24
+ _codex_snapshot() {
25
+ _CW_CODEX_BEFORE="$(_codex_rollouts)"
26
+ }
27
+
28
+ # unverified rollout layout: prints the id of the one new rollout naming the notes file, else nothing
29
+ codex_session_ref() {
30
+ local marker="${CW_NOTES_FILE:-}"
31
+ [[ -n "$marker" && "${CW_PROMPT:-}" == *"$marker"* ]] || return 0
32
+ CW_CX_BEFORE="${_CW_CODEX_BEFORE:-}" CW_CX_AFTER="$(_codex_rollouts)" CW_CX_MARKER="$marker" \
33
+ python3 - <<'PY' 2>/dev/null
34
+ import json, os, re
35
+ before = set(filter(None, os.environ["CW_CX_BEFORE"].split("\n")))
36
+ after = [p for p in os.environ["CW_CX_AFTER"].split("\n") if p and p not in before]
37
+ marker = os.environ["CW_CX_MARKER"]
38
+ def mentions(value):
39
+ if isinstance(value, str):
40
+ return marker in value
41
+ if isinstance(value, dict):
42
+ return any(mentions(v) for v in value.values())
43
+ if isinstance(value, list):
44
+ return any(mentions(v) for v in value)
45
+ return False
46
+ hits = []
47
+ for path in after:
48
+ try:
49
+ with open(path) as f:
50
+ if any(mentions(json.loads(line)) for line in f if line.strip()):
51
+ hits.append(path)
52
+ except Exception:
53
+ continue
54
+ uuid = r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$"
55
+ m = re.search(uuid, hits[0]) if len(hits) == 1 else None
56
+ if m:
57
+ print(m.group(1), end="")
58
+ PY
59
+ return 0
60
+ }
61
+
62
+ # sets only the top-level model keys in config.toml, refusing any file it cannot edit safely
63
+ _codex_write_provider() {
64
+ local provider="$1" model="$2"
65
+ [[ -n "$provider" && "$provider" != "native" ]] || return 0
66
+ # said once per command, however many launch attempts ask again
67
+ [[ -z "${_CW_CODEX_REFUSED:-}" ]] || return 1
68
+ mkdir -p "$CW_HARNESS_DIR"
69
+ local why
70
+ why=$(CW_TOML="$CW_HARNESS_DIR/config.toml" CW_TOML_PROVIDER="$provider" CW_TOML_MODEL="$model" \
71
+ python3 - <<'PY'
72
+ import os, re, shutil, sys, tempfile
73
+ path = os.environ["CW_TOML"]
74
+ want = [("model_provider", os.environ["CW_TOML_PROVIDER"]), ("model", os.environ["CW_TOML_MODEL"])]
75
+
76
+ def refuse(msg):
77
+ print(msg, end="")
78
+ sys.exit(3)
79
+
80
+ # a toml basic string; every control character and DEL goes out as a \u escape
81
+ def enc(v):
82
+ out = []
83
+ for ch in v:
84
+ o = ord(ch)
85
+ if ch == '"':
86
+ out.append('\\"')
87
+ elif ch == "\\":
88
+ out.append("\\\\")
89
+ elif o < 0x20 or o == 0x7F:
90
+ out.append("\\u%04x" % o)
91
+ else:
92
+ out.append(ch)
93
+ return '"' + "".join(out) + '"'
94
+
95
+ # bracket depth left open by these lines, ignoring strings and comments
96
+ def depth(lines):
97
+ d = 0
98
+ for line in lines:
99
+ i = 0
100
+ while i < len(line):
101
+ c = line[i]
102
+ if c == "#":
103
+ break
104
+ if c == '"':
105
+ i += 1
106
+ while i < len(line) and line[i] != '"':
107
+ i += 2 if line[i] == "\\" else 1
108
+ elif c == "'":
109
+ i += 1
110
+ while i < len(line) and line[i] != "'":
111
+ i += 1
112
+ elif c in "[{":
113
+ d += 1
114
+ elif c in "]}":
115
+ d -= 1
116
+ i += 1
117
+ return d
118
+
119
+ try:
120
+ with open(path) as f:
121
+ original = f.read()
122
+ except FileNotFoundError:
123
+ original = None
124
+ except Exception as e:
125
+ refuse(f"cannot read it ({e})")
126
+
127
+ lines = [] if original is None else original.splitlines(keepends=True)
128
+ first_table = next((i for i, l in enumerate(lines) if l.lstrip().startswith("[")), len(lines))
129
+ top = lines[:first_table]
130
+ if any(q in l for l in top for q in ('"' * 3, "'" * 3)):
131
+ refuse("its top level holds a multi-line string")
132
+ if depth(top) != 0:
133
+ refuse("its top level holds a multi-line array or table")
134
+
135
+ plain = re.compile(r"""^\s*[^=]+=\s*("(?:[^"\\]|\\.)*"|'[^']*')\s*(#.*)?$""")
136
+ for key, value in want:
137
+ if not value:
138
+ continue
139
+ new_line = f"{key} = {enc(value)}\n"
140
+ key_re = re.compile(r'^\s*(%s|"%s")\s*=' % (re.escape(key), re.escape(key)))
141
+ hits = [i for i, l in enumerate(lines[:first_table]) if key_re.match(l)]
142
+ if len(hits) > 1:
143
+ refuse(f"it sets {key} more than once")
144
+ if hits:
145
+ if not plain.match(lines[hits[0]].rstrip("\r\n")):
146
+ refuse(f"its {key} is not a plain one-line string")
147
+ lines[hits[0]] = new_line
148
+ else:
149
+ lines.insert(0, new_line)
150
+ first_table += 1
151
+
152
+ result = "".join(lines)
153
+ if result == original:
154
+ sys.exit(0)
155
+ try:
156
+ import tomllib
157
+ except ImportError:
158
+ tomllib = None
159
+ if tomllib is not None and original is not None:
160
+ try:
161
+ before, after = tomllib.loads(original), tomllib.loads(result)
162
+ except Exception:
163
+ refuse("it does not parse as toml")
164
+ for key, value in want:
165
+ if value:
166
+ before[key] = value
167
+ if before != after:
168
+ refuse("the edit would change more than the keys cw owns")
169
+ fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix=".config.toml.")
170
+ with os.fdopen(fd, "w") as f:
171
+ f.write(result)
172
+ if original is not None:
173
+ shutil.copymode(path, tmp)
174
+ os.replace(tmp, path)
175
+ PY
176
+ ) && return 0
177
+ _CW_CODEX_REFUSED=1
178
+ _err "Cannot apply provider '$provider' to $CW_HARNESS_DIR/config.toml: ${why:-unknown error}."
179
+ _err "Set model_provider and model in that file yourself; cw left it untouched."
180
+ return 1
181
+ }
182
+
183
+ _codex_base() {
184
+ _codex_write_provider "${CW_PROVIDER:-}" "${CW_MODEL:-}" || return 1
185
+ HARNESS_ENV=("CODEX_HOME=$CW_HARNESS_DIR")
186
+ if [[ -f "$CW_HARNESS_DIR/env" ]]; then
187
+ local -a extra_env=()
188
+ mapfile -t extra_env < <(_harness_env_file "$CW_HARNESS_DIR/env")
189
+ [[ ${#extra_env[@]} -gt 0 ]] && HARNESS_ENV+=("${extra_env[@]}")
190
+ fi
191
+ HARNESS_ARGV=(codex)
192
+ local f
193
+ # unverified: flag ordering around the resume subcommand (before vs after)
194
+ for f in $CW_EXTRA_FLAGS; do HARNESS_ARGV+=("$f"); done
195
+ return 0
196
+ }
197
+
198
+ # unverified: interactive launch argv beyond the bare binary
199
+ codex_launch() {
200
+ _codex_base || return 1
201
+ [[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$CW_MODEL")
202
+ [[ -n "$CW_PROMPT" ]] && HARNESS_ARGV+=("$CW_PROMPT")
203
+ _codex_snapshot
204
+ return 0
205
+ }
206
+
207
+ # unverified --last scope: offers the recorded id, then --last only where cw vouches for the dir
208
+ codex_resume() {
209
+ local attempt="$1"
210
+ local -a plan=()
211
+ [[ -n "$CW_SESSION_REF" ]] && plan+=(id)
212
+ [[ -n "${CW_CONTINUE_LAST_SAFE:-}" ]] && plan+=(last)
213
+ local step="${plan[$((attempt - 1))]:-}"
214
+ [[ -n "$step" ]] || return 1
215
+ _codex_base || return 1
216
+ case "$step" in
217
+ id) HARNESS_ARGV+=(resume "$CW_SESSION_REF") ;;
218
+ last) HARNESS_ARGV+=(resume --last) ;;
219
+ *) return 1 ;;
220
+ esac
221
+ [[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$CW_MODEL")
222
+ [[ -n "$CW_PROMPT" ]] && HARNESS_ARGV+=("$CW_PROMPT")
223
+ _codex_snapshot
224
+ return 0
225
+ }
226
+
227
+ codex_doctor() {
228
+ local status detail="null"
229
+ if ! command -v codex >/dev/null 2>&1; then
230
+ status="not_installed"; detail='"codex not found on PATH"'
231
+ elif [[ -f "$CW_HARNESS_DIR/auth.json" || -f "$CW_HARNESS_DIR/env" ]]; then
232
+ status="connected"
233
+ else
234
+ status="not_logged_in"
235
+ fi
236
+ printf '{"harness":"codex","status":"%s","detail":%s}\n' "$status" "$detail"
237
+ }
238
+
239
+ codex_login() {
240
+ HARNESS_ENV=("CODEX_HOME=$CW_HARNESS_DIR")
241
+ HARNESS_ARGV=(codex login)
242
+ [[ -n "${CW_LOGIN_NO_BROWSER:-}" ]] && HARNESS_ARGV+=(--device-auth)
243
+ [[ -n "${CW_LOGIN_API_KEY_STDIN:-}" ]] && HARNESS_ARGV=(codex login --with-api-key)
244
+ return 0
245
+ }
@@ -0,0 +1,120 @@
1
+ # opencode driver
2
+ # continue_last is not claimed: its --continue has no confirmed per-worktree scope
3
+ opencode_supports() {
4
+ local cap
5
+ for cap in non_interactive_prompt model_flag custom_provider; do
6
+ [[ "$1" == "$cap" ]] && return 0
7
+ done
8
+ return 1
9
+ }
10
+
11
+ opencode_config_env() {
12
+ printf 'OPENCODE_DATA_DIR=%s\n' "$CW_HARNESS_DIR"
13
+ printf 'OPENCODE_CONFIG=%s\n' "$CW_HARNESS_DIR/opencode.json"
14
+ }
15
+
16
+ # opencode has no name cw controls; no confirmed way to read one back either
17
+ opencode_session_ref() {
18
+ printf '%s' ""
19
+ }
20
+
21
+ _opencode_base() {
22
+ HARNESS_ENV=("OPENCODE_DATA_DIR=$CW_HARNESS_DIR"
23
+ "OPENCODE_CONFIG=$CW_HARNESS_DIR/opencode.json")
24
+ if [[ -f "$CW_HARNESS_DIR/env" ]]; then
25
+ local -a extra_env=()
26
+ mapfile -t extra_env < <(_harness_env_file "$CW_HARNESS_DIR/env")
27
+ [[ ${#extra_env[@]} -gt 0 ]] && HARNESS_ENV+=("${extra_env[@]}")
28
+ fi
29
+ HARNESS_ARGV=(opencode)
30
+ local f
31
+ # unverified: flag ordering, and whether these flags exist on opencode at all
32
+ for f in $CW_EXTRA_FLAGS; do HARNESS_ARGV+=("$f"); done
33
+ return 0
34
+ }
35
+
36
+ # prints the provider/model ref opencode expects; a native provider adds no prefix
37
+ _opencode_model_ref() {
38
+ local provider="${CW_PROVIDER:-native}" model="$CW_MODEL"
39
+ if [[ -z "$provider" || "$provider" == "native" || "$model" == "$provider/"* ]]; then
40
+ printf '%s' "$model"
41
+ else
42
+ printf '%s/%s' "$provider" "$model"
43
+ fi
44
+ }
45
+
46
+ # writes the provider and model into the account's opencode config
47
+ _opencode_write_config() {
48
+ [[ -n "$CW_PROVIDER" && "$CW_PROVIDER" != "native" ]] || return 0
49
+ mkdir -p "$CW_HARNESS_DIR"
50
+ local why
51
+ why=$(CW_OC_FILE="$CW_HARNESS_DIR/opencode.json" CW_OC_MODEL="$(_opencode_model_ref)" \
52
+ python3 - <<'PY'
53
+ import json, os, sys
54
+ p = os.environ["CW_OC_FILE"]
55
+ model = os.environ["CW_OC_MODEL"]
56
+ try:
57
+ with open(p) as f:
58
+ cfg = json.load(f)
59
+ except FileNotFoundError:
60
+ cfg = {}
61
+ except Exception:
62
+ print("it is not plain json cw can edit", end="")
63
+ sys.exit(3)
64
+ if not isinstance(cfg, dict):
65
+ print("it is not a json object", end="")
66
+ sys.exit(3)
67
+ if not model or cfg.get("model") == model:
68
+ sys.exit(0)
69
+ cfg["model"] = model
70
+ with open(p, "w") as f:
71
+ json.dump(cfg, f, indent=2)
72
+ PY
73
+ ) && return 0
74
+ _err "Cannot apply model '$(_opencode_model_ref)' to $CW_HARNESS_DIR/opencode.json: ${why:-unknown error}."
75
+ _err "Set \"model\" in that file yourself; cw left it untouched."
76
+ return 1
77
+ }
78
+
79
+ # unverified: --model flag ordering, whether the prompt can be passed on argv for a bare launch
80
+ opencode_launch() {
81
+ _opencode_write_config || return 1
82
+ _opencode_base
83
+ if [[ -n "$CW_PROMPT" ]]; then
84
+ HARNESS_ARGV=(opencode run "$CW_PROMPT")
85
+ [[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$(_opencode_model_ref)")
86
+ elif [[ -n "$CW_MODEL" ]]; then
87
+ HARNESS_ARGV+=(--model "$(_opencode_model_ref)")
88
+ fi
89
+ return 0
90
+ }
91
+
92
+ # only a recorded id is attributable; --continue has no confirmed worktree scope, so it is unused
93
+ opencode_resume() {
94
+ local attempt="$1"
95
+ [[ "$attempt" == "1" && -n "$CW_SESSION_REF" ]] || return 1
96
+ _opencode_write_config || return 1
97
+ _opencode_base
98
+ HARNESS_ARGV=(opencode run "$CW_PROMPT" --session "$CW_SESSION_REF")
99
+ [[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$(_opencode_model_ref)")
100
+ return 0
101
+ }
102
+
103
+ opencode_doctor() {
104
+ local status detail="null"
105
+ if ! command -v opencode >/dev/null 2>&1; then
106
+ status="not_installed"; detail='"opencode not found on PATH"'
107
+ elif [[ -f "$CW_HARNESS_DIR/auth.json" || -f "$CW_HARNESS_DIR/env" ]]; then
108
+ status="connected"
109
+ else
110
+ status="not_logged_in"
111
+ fi
112
+ printf '{"harness":"opencode","status":"%s","detail":%s}\n' "$status" "$detail"
113
+ }
114
+
115
+ # unverified: no confirmed --no-browser/device-code flag or api-key import path for opencode
116
+ opencode_login() {
117
+ HARNESS_ENV=("OPENCODE_DATA_DIR=$CW_HARNESS_DIR")
118
+ HARNESS_ARGV=(opencode auth login)
119
+ return 0
120
+ }
@@ -0,0 +1,66 @@
1
+ # pi coding agent driver
2
+ # unverified: entire capability set below is inferred from docs, no binary to check against
3
+ # no custom_provider: this driver does not apply CW_PROVIDER yet
4
+ pi_supports() {
5
+ local cap
6
+ for cap in model_flag instructions_file; do
7
+ [[ "$1" == "$cap" ]] && return 0
8
+ done
9
+ return 1
10
+ }
11
+
12
+ pi_config_env() {
13
+ printf 'PI_CODING_AGENT_DIR=%s\n' "$CW_HARNESS_DIR"
14
+ }
15
+
16
+ # pi has no name cw controls; no confirmed way to read one back either
17
+ pi_session_ref() {
18
+ printf '%s' ""
19
+ }
20
+
21
+ _pi_base() {
22
+ HARNESS_ENV=("PI_CODING_AGENT_DIR=$CW_HARNESS_DIR")
23
+ if [[ -f "$CW_HARNESS_DIR/env" ]]; then
24
+ local -a extra_env=()
25
+ mapfile -t extra_env < <(_harness_env_file "$CW_HARNESS_DIR/env")
26
+ [[ ${#extra_env[@]} -gt 0 ]] && HARNESS_ENV+=("${extra_env[@]}")
27
+ fi
28
+ HARNESS_ARGV=(pi)
29
+ local f
30
+ # unverified: flag ordering and whether these flags exist on pi at all
31
+ for f in $CW_EXTRA_FLAGS; do HARNESS_ARGV+=("$f"); done
32
+ return 0
33
+ }
34
+
35
+ # unverified: --model flag, and whether a prompt can be passed on argv at all
36
+ pi_launch() {
37
+ _pi_base
38
+ [[ -n "$CW_MODEL" ]] && HARNESS_ARGV+=(--model "$CW_MODEL")
39
+ [[ -n "$CW_PROMPT" ]] && HARNESS_ARGV+=("$CW_PROMPT")
40
+ return 0
41
+ }
42
+
43
+ # unverified resume mechanism, so no attempt is offered and cw starts fresh from the notes
44
+ pi_resume() {
45
+ return 1
46
+ }
47
+
48
+ pi_doctor() {
49
+ local status detail="null"
50
+ if ! command -v pi >/dev/null 2>&1; then
51
+ status="not_installed"; detail='"pi not found on PATH"'
52
+ elif [[ -f "$CW_HARNESS_DIR/auth.json" || -f "$CW_HARNESS_DIR/env" ]]; then
53
+ status="connected"
54
+ else
55
+ status="not_logged_in"
56
+ fi
57
+ printf '{"harness":"pi","status":"%s","detail":%s}\n' "$status" "$detail"
58
+ }
59
+
60
+ # unverified: login is documented as an in-session /login command, not a subcommand or flag
61
+ # cw can only drop the user into a bare interactive session to run it themselves
62
+ pi_login() {
63
+ HARNESS_ENV=("PI_CODING_AGENT_DIR=$CW_HARNESS_DIR")
64
+ HARNESS_ARGV=(pi)
65
+ return 0
66
+ }
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # CW iTerm2 Helpers — AppleScript + escape sequences
4
+ # ============================================================================
5
+
6
+ # ── Color presets per mode ─────────────────────────────────────────────────
7
+
8
+ _mode_color() {
9
+ case "$1" in
10
+ code) echo "50 120 220" ;;
11
+ review) echo "180 80 200" ;;
12
+ research) echo "40 180 160" ;;
13
+ docs) echo "220 160 40" ;;
14
+ planning) echo "60 180 75" ;;
15
+ comms) echo "200 60 80" ;;
16
+ full) echo "100 100 100" ;;
17
+ shell) echo "80 80 80" ;;
18
+ git) echo "230 120 50" ;;
19
+ feature) echo "50 150 250" ;;
20
+ bug) echo "230 60 60" ;;
21
+ *) echo "100 100 100" ;;
22
+ esac
23
+ }
24
+
25
+ # ── Core AppleScript functions ─────────────────────────────────────────────
26
+
27
+ # Create a new iTerm2 window, cd to dir
28
+ _iterm_new_window() {
29
+ local dir="$1"
30
+ osascript <<EOF
31
+ tell application "iTerm2"
32
+ set newWindow to (create window with default profile)
33
+ tell current session of current tab of newWindow
34
+ write text "cd ${dir}"
35
+ end tell
36
+ end tell
37
+ EOF
38
+ }
39
+
40
+ # Create a new tab in current window, cd to dir
41
+ _iterm_new_tab() {
42
+ local dir="$1"
43
+ osascript <<EOF
44
+ tell application "iTerm2"
45
+ tell current window
46
+ set newTab to (create tab with default profile)
47
+ tell current session of newTab
48
+ write text "cd ${dir}"
49
+ end tell
50
+ end tell
51
+ end tell
52
+ EOF
53
+ }
54
+
55
+ # Send a command to current session
56
+ _iterm_send() {
57
+ local cmd="$1"
58
+ osascript <<EOF
59
+ tell application "iTerm2"
60
+ tell current session of current tab of current window
61
+ write text "${cmd}"
62
+ end tell
63
+ end tell
64
+ EOF
65
+ }
66
+
67
+ # Select tab by index (1-based)
68
+ _iterm_select_tab() {
69
+ local idx="$1"
70
+ osascript <<EOF
71
+ tell application "iTerm2"
72
+ tell current window
73
+ select tab ${idx}
74
+ end tell
75
+ end tell
76
+ EOF
77
+ }
78
+
79
+ # Focus iTerm2
80
+ _iterm_focus() {
81
+ osascript -e 'tell application "iTerm2" to activate'
82
+ }
83
+
84
+ # ── Tab coloring via escape sequences ──────────────────────────────────────
85
+
86
+ # Set tab color on current session (must be called AFTER the tab exists)
87
+ _iterm_set_tab_color() {
88
+ local r="$1" g="$2" b="$3"
89
+ _iterm_send "printf '\\\\033]6;1;bg;red;brightness;${r}\\\\a\\\\033]6;1;bg;green;brightness;${g}\\\\a\\\\033]6;1;bg;blue;brightness;${b}\\\\a'"
90
+ }
91
+
92
+ # Set badge on current session
93
+ _iterm_set_badge() {
94
+ local text="$1"
95
+ local b64
96
+ b64=$(printf '%s' "$text" | base64)
97
+ _iterm_send "printf '\\\\033]1337;SetBadgeFormat=${b64}\\\\a'"
98
+ }
99
+
100
+ # Set tab title via escape sequence
101
+ _iterm_set_title() {
102
+ local title="$1"
103
+ _iterm_send "printf '\\\\033]0;${title}\\\\007'"
104
+ }
105
+
106
+ # ── Compound: configured window and tabs ──────────────────────────────────
107
+
108
+ # Create a fully configured window: dir, command, badge, mode
109
+ _iterm_configured_window() {
110
+ local title="$1" dir="$2" cmd="$3" badge="$4" mode="$5"
111
+ local r g b
112
+ read -r r g b <<< "$(_mode_color "$mode")"
113
+
114
+ _iterm_new_window "$dir"
115
+ sleep 0.4
116
+
117
+ _iterm_set_title "$title"
118
+ _iterm_set_tab_color "$r" "$g" "$b"
119
+ [[ -n "$badge" ]] && _iterm_set_badge "$badge"
120
+
121
+ sleep 0.2
122
+ [[ -n "$cmd" ]] && _iterm_send "$cmd"
123
+ }
124
+
125
+ # Create a fully configured tab: dir, command, badge, mode
126
+ _iterm_configured_tab() {
127
+ local title="$1" dir="$2" cmd="$3" badge="$4" mode="$5"
128
+ local r g b
129
+ read -r r g b <<< "$(_mode_color "$mode")"
130
+
131
+ _iterm_new_tab "$dir"
132
+ sleep 0.4
133
+
134
+ _iterm_set_title "$title"
135
+ _iterm_set_tab_color "$r" "$g" "$b"
136
+ [[ -n "$badge" ]] && _iterm_set_badge "$badge"
137
+
138
+ sleep 0.2
139
+ [[ -n "$cmd" ]] && _iterm_send "$cmd"
140
+ }
@@ -0,0 +1,48 @@
1
+ # [Project Name]
2
+
3
+ ## What is this
4
+ <!-- One line: what this project does -->
5
+
6
+ ## Stack
7
+ <!-- Tech stack as table or short list -->
8
+
9
+ ## Structure
10
+ ```
11
+ src/
12
+ ├── ...
13
+ ```
14
+
15
+ ## Development
16
+ ```bash
17
+ # Setup
18
+ # Dev
19
+ # Test
20
+ # Build
21
+ # Deploy
22
+ ```
23
+
24
+ ## Conventions
25
+ - Conventional commits: `feat:`, `fix:`, `chore:`
26
+ - Tests required for all new features
27
+ - No direct pushes to main
28
+ - Comments & notes: single line only, no docstring-style or multi-line blocks — in code and in TASK_NOTES.md alike; never reference task IDs, branch names, or GitHub/Linear issue numbers
29
+ <!-- Add project-specific conventions -->
30
+
31
+ ## Working with me (Claude)
32
+ - If you don't know something about the project, ask. Don't make it up.
33
+ - Before major changes, explain your plan first.
34
+ - Run tests after every change.
35
+ - If you see a TASK_NOTES.md or REVIEW_NOTES.md in the working directory, read it first — it has context from previous sessions. The actual file lives in `~/.cw/sessions/` and is symlinked here.
36
+ - Never touch `.env`, credentials, or secrets. Only reference them.
37
+
38
+ ## Do NOT
39
+ - Do not change infrastructure config without confirmation
40
+ - Do not modify existing migrations
41
+ - Do not delete tests
42
+ <!-- Add project-specific restrictions -->
43
+
44
+ ## Integrations
45
+ <!-- MCPs, APIs, external services -->
46
+
47
+ ## Additional context
48
+ <!-- Links to docs, ADRs, important decisions -->