toga-ai 1.0.984 → 1.0.986

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.
@@ -1,310 +1,307 @@
1
1
  {
2
- "hooks": {
3
- "UserPromptSubmit": [
4
- {
5
- "hooks": [
6
- {
7
- "type": "command",
8
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/start-gate.js\"",
9
- "timeout": 3000
10
- }
11
- ]
12
- },
13
- {
14
- "hooks": [
15
- {
16
- "type": "command",
17
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/plain-talk.js\"",
18
- "timeout": 3000
19
- }
20
- ]
21
- },
22
- {
23
- "hooks": [
24
- {
25
- "type": "command",
26
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/session-files.js\"",
27
- "timeout": 3000
28
- }
29
- ]
30
- }
31
- ],
32
- "PreToolUse": [
33
- {
34
- "matcher": "Bash|PowerShell|Read|Grep|Glob|Edit|Write|MultiEdit|NotebookEdit|Task|Agent|WebFetch|WebSearch|mcp__.*[Cc][Ll][Ii][Cc][Kk][Uu][Pp].*",
35
- "hooks": [
36
- {
37
- "type": "command",
38
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/start-gate.js\"",
39
- "timeout": 3000
40
- }
41
- ]
42
- },
43
- {
44
- "matcher": "Bash",
45
- "hooks": [
46
- {
47
- "type": "command",
48
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/block-destructive.js\"",
49
- "timeout": 3000
50
- }
51
- ]
52
- },
53
- {
54
- "matcher": "Write|Edit|MultiEdit",
55
- "hooks": [
56
- {
57
- "type": "command",
58
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/guard-index.js\"",
59
- "timeout": 3000
60
- }
61
- ]
62
- },
63
- {
64
- "matcher": "Write|Edit|MultiEdit",
65
- "hooks": [
66
- {
67
- "type": "command",
68
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/dbchanges2-cluster-isolation.js\"",
69
- "timeout": 3000
70
- }
71
- ]
72
- },
73
- {
74
- "matcher": "Write|Edit|MultiEdit",
75
- "hooks": [
76
- {
77
- "type": "command",
78
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/dbchanges2-record-ids.js\"",
79
- "timeout": 3000
80
- }
81
- ]
82
- },
83
- {
84
- "matcher": "Write|Edit|MultiEdit",
85
- "hooks": [
86
- {
87
- "type": "command",
88
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/sql-file-location.js\"",
89
- "timeout": 3000
90
- }
91
- ]
92
- },
93
- {
94
- "matcher": "Edit|Write|MultiEdit",
95
- "hooks": [
96
- {
97
- "type": "command",
98
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/design-fidelity-reminder.js\"",
99
- "timeout": 3000
100
- }
101
- ]
102
- },
103
- {
104
- "matcher": "Write|Edit",
105
- "hooks": [
106
- {
107
- "type": "command",
108
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/platform-reuse-reminder.js\"",
109
- "timeout": 3000
110
- }
111
- ]
112
- }
113
- ],
114
- "PostToolUseFailure": [
115
- {
116
- "matcher": "mcp__.*[Cc][Ll][Ii][Cc][Kk][Uu][Pp].*",
117
- "hooks": [
118
- {
119
- "type": "command",
120
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/start-gate.js\"",
121
- "timeout": 3000
122
- }
123
- ]
124
- }
125
- ],
126
- "PostToolUse": [
127
- {
128
- "matcher": "Bash|PowerShell|mcp__.*[Cc][Ll][Ii][Cc][Kk][Uu][Pp].*",
129
- "hooks": [
130
- {
131
- "type": "command",
132
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/start-gate.js\"",
133
- "timeout": 3000
134
- }
135
- ]
136
- },
137
- {
138
- "matcher": "Write|Edit|MultiEdit",
139
- "hooks": [
140
- {
141
- "type": "command",
142
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/post-edit-validate.js\"",
143
- "timeout": 15000
144
- }
145
- ]
146
- },
147
- {
148
- "matcher": "Write|Edit|MultiEdit",
149
- "hooks": [
150
- {
151
- "type": "command",
152
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/remind-php-review.js\"",
153
- "timeout": 3000
154
- }
155
- ]
156
- },
157
- {
158
- "matcher": "Write|Edit|MultiEdit",
159
- "hooks": [
160
- {
161
- "type": "command",
162
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/remind-sql-review.js\"",
163
- "timeout": 3000
164
- }
165
- ]
166
- },
167
- {
168
- "matcher": "Write|Edit|MultiEdit",
169
- "hooks": [
170
- {
171
- "type": "command",
172
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/scope-counter.js\"",
173
- "timeout": 3000
174
- }
175
- ]
176
- },
177
- {
178
- "matcher": "Write|Edit|MultiEdit",
179
- "hooks": [
180
- {
181
- "type": "command",
182
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/remind-frontend-review.js\"",
183
- "timeout": 3000
184
- }
185
- ]
186
- },
187
- {
188
- "matcher": "Write|Edit|MultiEdit",
189
- "hooks": [
190
- {
191
- "type": "command",
192
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/sql-cross-cluster-check.js\"",
193
- "timeout": 3000
194
- }
195
- ]
196
- },
197
- {
198
- "matcher": "Write|Edit|MultiEdit",
199
- "hooks": [
200
- {
201
- "type": "command",
202
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/dbchanges2-file-sprawl.js\"",
203
- "timeout": 3000
204
- }
205
- ]
206
- },
207
- {
208
- "matcher": "Write|Edit|MultiEdit",
209
- "hooks": [
210
- {
211
- "type": "command",
212
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/memory-scope-guard.js\"",
213
- "timeout": 3000
214
- }
215
- ]
216
- },
217
- {
218
- "matcher": "Edit|Write|MultiEdit|NotebookEdit",
219
- "hooks": [
220
- {
221
- "type": "command",
222
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/session-files.js\"",
223
- "timeout": 3000
224
- }
225
- ]
226
- }
227
- ],
228
- "SessionStart": [
229
- {
230
- "hooks": [
231
- {
232
- "type": "command",
233
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/session-start.js\"",
234
- "timeout": 5000
235
- }
236
- ]
237
- }
238
- ],
239
- "Stop": [
240
- {
241
- "hooks": [
242
- {
243
- "type": "command",
244
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/session-end.js\"",
245
- "timeout": 5000
246
- }
247
- ]
248
- },
249
- {
250
- "hooks": [
251
- {
252
- "type": "command",
253
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/evaluate-session.js\"",
254
- "timeout": 5000
255
- }
256
- ]
257
- },
258
- {
259
- "hooks": [
260
- {
261
- "type": "command",
262
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/remind-php-lint.js\"",
263
- "timeout": 3000
264
- }
265
- ]
266
- }
267
- ]
268
- },
269
- "permissions": {
270
- "allow": [
271
- "Bash(node .claude/knowledge.js *)",
272
- "Bash(node .claude/hooks/toga/*.js)",
273
- "Bash(node sync-skills.js *)",
274
- "Bash(node scripts/harness.js *)",
275
- "Bash(node scripts/install.js *)",
276
- "Bash(php -l *)",
277
- "Bash(git status)",
278
- "Bash(git diff *)",
279
- "Bash(git log *)",
280
- "Bash(git add *)",
281
- "Bash(git commit *)",
282
- "Bash(git checkout *)",
283
- "Bash(git branch *)",
284
- "Bash(git pull *)",
285
- "Bash(npm *)",
286
- "Bash(npx *)"
287
- ],
288
- "ask": [
289
- "mcp__clickup__clickup_update_task",
290
- "Bash(git push *)"
291
- ],
292
- "deny": [
293
- "mcp__clickup__clickup_delete_task",
294
- "mcp__clickup__clickup_delete_comment",
295
- "mcp__clickup__clickup_create_task",
296
- "mcp__clickup__clickup_execute_operator",
297
- "mcp__clickup__clickup_merge_tasks",
298
- "mcp__clickup__clickup_move_task",
299
- "mcp__claude_ai_ClickUP_for_Dev",
300
- "Bash(git push --force*)",
301
- "Bash(git push -f*)",
302
- "Bash(git push *--force*)",
303
- "Bash(git push * -f*)",
304
- "Bash(git push * +*)"
305
- ]
306
- },
307
- "env": {
308
- "GATEGUARD_BASH_ROUTINE_DISABLED": "1"
309
- }
2
+ "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/start-gate.js\"",
9
+ "timeout": 3000
10
+ }
11
+ ]
12
+ },
13
+ {
14
+ "hooks": [
15
+ {
16
+ "type": "command",
17
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/plain-talk.js\"",
18
+ "timeout": 3000
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "hooks": [
24
+ {
25
+ "type": "command",
26
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/session-files.js\"",
27
+ "timeout": 3000
28
+ }
29
+ ]
30
+ }
31
+ ],
32
+ "PreToolUse": [
33
+ {
34
+ "matcher": "Bash|PowerShell|Read|Grep|Glob|Edit|Write|MultiEdit|NotebookEdit|Task|Agent|WebFetch|WebSearch|mcp__.*[Cc][Ll][Ii][Cc][Kk][Uu][Pp].*",
35
+ "hooks": [
36
+ {
37
+ "type": "command",
38
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/start-gate.js\"",
39
+ "timeout": 3000
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ "matcher": "Bash",
45
+ "hooks": [
46
+ {
47
+ "type": "command",
48
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/block-destructive.js\"",
49
+ "timeout": 3000
50
+ }
51
+ ]
52
+ },
53
+ {
54
+ "matcher": "Write|Edit|MultiEdit",
55
+ "hooks": [
56
+ {
57
+ "type": "command",
58
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/guard-index.js\"",
59
+ "timeout": 3000
60
+ }
61
+ ]
62
+ },
63
+ {
64
+ "matcher": "Write|Edit|MultiEdit",
65
+ "hooks": [
66
+ {
67
+ "type": "command",
68
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/dbchanges2-cluster-isolation.js\"",
69
+ "timeout": 3000
70
+ }
71
+ ]
72
+ },
73
+ {
74
+ "matcher": "Write|Edit|MultiEdit",
75
+ "hooks": [
76
+ {
77
+ "type": "command",
78
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/dbchanges2-record-ids.js\"",
79
+ "timeout": 3000
80
+ }
81
+ ]
82
+ },
83
+ {
84
+ "matcher": "Write|Edit|MultiEdit",
85
+ "hooks": [
86
+ {
87
+ "type": "command",
88
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/sql-file-location.js\"",
89
+ "timeout": 3000
90
+ }
91
+ ]
92
+ },
93
+ {
94
+ "matcher": "Edit|Write|MultiEdit",
95
+ "hooks": [
96
+ {
97
+ "type": "command",
98
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/design-fidelity-reminder.js\"",
99
+ "timeout": 3000
100
+ }
101
+ ]
102
+ },
103
+ {
104
+ "matcher": "Write|Edit",
105
+ "hooks": [
106
+ {
107
+ "type": "command",
108
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/platform-reuse-reminder.js\"",
109
+ "timeout": 3000
110
+ }
111
+ ]
112
+ }
113
+ ],
114
+ "PostToolUseFailure": [
115
+ {
116
+ "matcher": "mcp__.*[Cc][Ll][Ii][Cc][Kk][Uu][Pp].*",
117
+ "hooks": [
118
+ {
119
+ "type": "command",
120
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/start-gate.js\"",
121
+ "timeout": 3000
122
+ }
123
+ ]
124
+ }
125
+ ],
126
+ "PostToolUse": [
127
+ {
128
+ "matcher": "Bash|PowerShell|mcp__.*[Cc][Ll][Ii][Cc][Kk][Uu][Pp].*",
129
+ "hooks": [
130
+ {
131
+ "type": "command",
132
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/start-gate.js\"",
133
+ "timeout": 3000
134
+ }
135
+ ]
136
+ },
137
+ {
138
+ "matcher": "Write|Edit|MultiEdit",
139
+ "hooks": [
140
+ {
141
+ "type": "command",
142
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/post-edit-validate.js\"",
143
+ "timeout": 15000
144
+ }
145
+ ]
146
+ },
147
+ {
148
+ "matcher": "Write|Edit|MultiEdit",
149
+ "hooks": [
150
+ {
151
+ "type": "command",
152
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/remind-php-review.js\"",
153
+ "timeout": 3000
154
+ }
155
+ ]
156
+ },
157
+ {
158
+ "matcher": "Write|Edit|MultiEdit",
159
+ "hooks": [
160
+ {
161
+ "type": "command",
162
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/remind-sql-review.js\"",
163
+ "timeout": 3000
164
+ }
165
+ ]
166
+ },
167
+ {
168
+ "matcher": "Write|Edit|MultiEdit",
169
+ "hooks": [
170
+ {
171
+ "type": "command",
172
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/scope-counter.js\"",
173
+ "timeout": 3000
174
+ }
175
+ ]
176
+ },
177
+ {
178
+ "matcher": "Write|Edit|MultiEdit",
179
+ "hooks": [
180
+ {
181
+ "type": "command",
182
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/remind-frontend-review.js\"",
183
+ "timeout": 3000
184
+ }
185
+ ]
186
+ },
187
+ {
188
+ "matcher": "Write|Edit|MultiEdit",
189
+ "hooks": [
190
+ {
191
+ "type": "command",
192
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/sql-cross-cluster-check.js\"",
193
+ "timeout": 3000
194
+ }
195
+ ]
196
+ },
197
+ {
198
+ "matcher": "Write|Edit|MultiEdit",
199
+ "hooks": [
200
+ {
201
+ "type": "command",
202
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/dbchanges2-file-sprawl.js\"",
203
+ "timeout": 3000
204
+ }
205
+ ]
206
+ },
207
+ {
208
+ "matcher": "Write|Edit|MultiEdit",
209
+ "hooks": [
210
+ {
211
+ "type": "command",
212
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/memory-scope-guard.js\"",
213
+ "timeout": 3000
214
+ }
215
+ ]
216
+ },
217
+ {
218
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
219
+ "hooks": [
220
+ {
221
+ "type": "command",
222
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/session-files.js\"",
223
+ "timeout": 3000
224
+ }
225
+ ]
226
+ }
227
+ ],
228
+ "SessionStart": [
229
+ {
230
+ "hooks": [
231
+ {
232
+ "type": "command",
233
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/session-start.js\"",
234
+ "timeout": 5000
235
+ }
236
+ ]
237
+ }
238
+ ],
239
+ "Stop": [
240
+ {
241
+ "hooks": [
242
+ {
243
+ "type": "command",
244
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/session-end.js\"",
245
+ "timeout": 5000
246
+ }
247
+ ]
248
+ },
249
+ {
250
+ "hooks": [
251
+ {
252
+ "type": "command",
253
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/evaluate-session.js\"",
254
+ "timeout": 5000
255
+ }
256
+ ]
257
+ },
258
+ {
259
+ "hooks": [
260
+ {
261
+ "type": "command",
262
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/toga/remind-php-lint.js\"",
263
+ "timeout": 3000
264
+ }
265
+ ]
266
+ }
267
+ ]
268
+ },
269
+ "permissions": {
270
+ "allow": [
271
+ "Bash(node .claude/knowledge.js *)",
272
+ "Bash(node .claude/hooks/toga/*.js)",
273
+ "Bash(node sync-skills.js *)",
274
+ "Bash(node scripts/harness.js *)",
275
+ "Bash(node scripts/install.js *)",
276
+ "Bash(php -l *)",
277
+ "Bash(git status)",
278
+ "Bash(git diff *)",
279
+ "Bash(git log *)",
280
+ "Bash(git add *)",
281
+ "Bash(git commit *)",
282
+ "Bash(git checkout *)",
283
+ "Bash(git branch *)",
284
+ "Bash(git pull *)",
285
+ "Bash(npm *)",
286
+ "Bash(npx *)"
287
+ ],
288
+ "ask": ["Bash(git push *)"],
289
+ "deny": [
290
+ "mcp__clickup__clickup_delete_task",
291
+ "mcp__clickup__clickup_delete_comment",
292
+ "mcp__clickup__clickup_create_task",
293
+ "mcp__clickup__clickup_execute_operator",
294
+ "mcp__clickup__clickup_merge_tasks",
295
+ "mcp__clickup__clickup_move_task",
296
+ "mcp__claude_ai_ClickUP_for_Dev",
297
+ "Bash(git push --force*)",
298
+ "Bash(git push -f*)",
299
+ "Bash(git push *--force*)",
300
+ "Bash(git push * -f*)",
301
+ "Bash(git push * +*)"
302
+ ]
303
+ },
304
+ "env": {
305
+ "GATEGUARD_BASH_ROUTINE_DISABLED": "1"
306
+ }
310
307
  }
@@ -6,7 +6,7 @@ project: Claude Harness
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-08
9
+ updated: 2026-10-09
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - claude/skills/start/SKILL.md
@@ -36,7 +36,9 @@ writes, and agents until the session has a ticket. These replaced `/kickoff`, `/
36
36
 
37
37
  1. **`/start TRUE-1234`** — ticket id only (anything else is blocked). The `ticket-reader` agent
38
38
  reads the ticket (apps, clients, task type, pseudocode, comments) off the main thread. Then
39
- `/start` checks branches, primes the team KB, and loads PR review feedback if an open PR
39
+ `/start` builds the repo list from the ticket's Application values, checks only those repos'
40
+ branches, and offers to create/switch to the ticket branch (see below). It primes the team
41
+ KB, moves the status `to do` -> `in progress`, and loads PR review feedback if an open PR
40
42
  exists (rework: same branch, same PR). If ClickUp can't be read, it stops — no offline mode.
41
43
  2. **`/commit`** — commits only this session's files, ticket from the session file, then the
42
44
  branch. Never pushes.
@@ -47,12 +49,33 @@ writes, and agents until the session has a ticket. These replaced `/kickoff`, `/
47
49
  repo with work, open or update a PR per repo, post a PM comment, set status
48
50
  `development finished`.
49
51
 
52
+ ### Ticket branch at /start
53
+
54
+ - Application -> repo map lives in `skills/start/SKILL.md` Step 4 (e.g. TOGa Desk ->
55
+ `toga25-desk`, API2 -> `api2`, Team Knowledge Base -> KB repo only, no branch work). The dev
56
+ can add or drop repos.
57
+ - `start-helper.js branch-check --repos <a,b>` checks only those repos (match / mismatch /
58
+ missing). Without `--repos`, the old scan of every repo under the root still runs.
59
+ - For each mismatch Claude asks once "Create/switch to `<T>` in `<repo>` from `<base>`?" (base
60
+ from `gh repo view <owner/repo> --json defaultBranchRef`). On yes:
61
+ `start-helper.js create-branch --session <sid> --repo <path> --base <base>` — checks out
62
+ `origin/<T>` if it exists, else local `<T>`, else creates `<T>` from `origin/<base>` and runs
63
+ `git push -u origin <T>` (never force). Refuses on uncommitted tracked changes (never
64
+ stashes), on the KB repo, and when the session ticket is not verified.
65
+ - This is the one branch exception in `rules/common/git-workflow.md`.
66
+
67
+ ### Status flow
68
+
69
+ `to do` -> `in progress` (`/start`, only from `to do`, never backward) -> `development finished`
70
+ (`/finish`). The `start-gate` guard allows only those two status values.
71
+
50
72
  ### Ticket-write rules
51
73
 
52
74
  - Writes go only to the session's own ticket — the `start-gate` hook blocks any other task id,
53
75
  and blocks task creation, deletion, moves, tags, links, docs, chat, and time tracking.
54
- - `permissions.ask` makes Claude Code ask the developer before every `clickup_update_task` and
55
- `clickup_create_comment`.
76
+ - `permissions.ask` makes Claude Code ask the developer before every `clickup_update_task`
77
+ (it cannot be scoped to one argument, so the status change asks too). `clickup_create_comment`
78
+ posts without a prompt.
56
79
  - Use only the `clickup` MCP server (`mcp__clickup__clickup_*`). The old read-only
57
80
  "claude.ai ClickUP for Dev" server (`mcp__claude_ai_ClickUP_for_Dev__*`) is blocked.
58
81
 
@@ -100,7 +123,9 @@ All tested 2026-10-08:
100
123
  recorded — Claude never writes it. It auto-checks Has UI Changes when a ticket enters a Dev
101
124
  Sprints list; `/finish` keeps it checked only if the session changed UI files (`.jsx .tsx .css
102
125
  .scss .less .html`, a React app's `src/`, blox components), else unchecks it.
103
- - **Branch = bare ticket id**, created by ClickUp. PR base = the repo's default branch (usually
126
+ - **Branch = bare ticket id**, created by ClickUp or by `/start`'s `create-branch`. A branch
127
+ named with the ticket id should link to the ticket through the GitHub integration — confirm on
128
+ the first real `create-branch` run. PR base = the repo's default branch (usually
104
129
  `_production`, varies) — resolve with `gh repo view <owner/repo> --json defaultBranchRef`.
105
130
  - **The KB repo (`toga-tech`) works on `_main`.** No branch check and no PRs there; `/finish`
106
131
  still does the ticket check.
@@ -127,6 +152,10 @@ All tested 2026-10-08:
127
152
  pushes still ask. (jcardinal)
128
153
  - 2026-10-08 — Added big-read/saved-file behavior, `gh auth login` note, and PR summary Action. (jcardinal)
129
154
  - 2026-10-08 — Dropped the PR summary Action: `/finish` writes the review summary into the PR description. (jcardinal)
155
+ - 2026-10-09 — `/start` builds the repo list from Application, `branch-check --repos` checks only
156
+ those, and `create-branch` creates/switches the ticket branch after the dev's yes. (jcardinal)
157
+ - 2026-10-09 — `/start` sets status `to do` -> `in progress`; the gate allows `in progress` and
158
+ `development finished` only. (jcardinal)
130
159
 
131
160
  ## Related
132
161
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.984",
3
+ "version": "1.0.986",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",
@@ -58,6 +58,11 @@ them switch (`git checkout` / `git switch`) — Claude does **not** switch branc
58
58
  either (see *Claude never rewrites code because a branch changed*). Present diffs/patches and
59
59
  let the developer commit, PR, and deploy.
60
60
 
61
+ **One exception — `/start`'s `create-branch`.** At `/start`, after the developer says yes for
62
+ that repo, Claude may run `start-helper.js create-branch` to create (from the default branch)
63
+ or check out the ticket branch `<TICKET>`. Nothing else: no worktree, no stash, never on a
64
+ repo with uncommitted changes, never force-push. See *Branch naming*.
65
+
61
66
  ## Claude never rewrites code because a branch changed
62
67
 
63
68
  Many developers work across many projects and switch branches constantly. A change that is
@@ -117,10 +122,15 @@ Never force-push to `_main` under any circumstances.
117
122
  ## Branch naming
118
123
 
119
124
  The branch is the bare ClickUp ticket id — e.g. `TRUE-82600`. No prefix, no description.
120
- ClickUp creates it. One branch per ticket per repo.
121
-
122
- `/start` checks that every repo with work is on the ticket branch. On a mismatch it stops and
123
- says which repo is on which branch. Claude never switches branches itself.
125
+ ClickUp may create it, or `/start` creates it. One branch per ticket per repo.
126
+
127
+ `/start` checks that every repo on the ticket (from its Application values) is on the ticket
128
+ branch. On a mismatch it asks once per repo: "Create/switch to `<TICKET>` in `<repo>` from
129
+ `<base>`?" On yes it runs `start-helper.js create-branch` (checks out `origin/<TICKET>` or the
130
+ local branch if one exists, else creates it from the default branch and pushes it with
131
+ `git push -u`, never force). A repo with uncommitted changes is refused — Claude stops and
132
+ says so. This is the only case where Claude creates or switches a branch; otherwise Claude
133
+ never switches branches itself.
124
134
 
125
135
  The KB/harness repo (`toga-tech`) is the exception: work there goes on `_main`.
126
136
 
@@ -15,8 +15,9 @@
15
15
  * Stages (PreToolUse), from the session file:
16
16
  * none no file (never ran /start). Block Edit/Write/MultiEdit/NotebookEdit and Agent/Task.
17
17
  * Reads, searches, Bash and MCP reads stay allowed (answering questions is fine).
18
- * ticket state "pending". Only the ticket-read steps: ClickUp reads, the pinned start/ scripts,
19
- * gh auth status / gh pr list|view|checks, plain git reads, KB + ~/.claude + temp reads,
18
+ * ticket state "pending". Only the ticket-read steps: ClickUp reads, the pinned start/ scripts
19
+ * (incl. `start-helper.js create-branch`, which refuses until verified),
20
+ * gh auth status / gh pr list|view|checks / gh repo view, plain git reads, KB + ~/.claude + temp reads,
20
21
  * and an Agent spawn of subagent_type `ticket-reader` only.
21
22
  * primer state "verified", not primed. Old kickoff stage 2: KB reads, INVENTORY.md / APPS.md
22
23
  * reads, edits under the team repo knowledge/, and read-only knowledge.js commands.
@@ -32,7 +33,8 @@
32
33
  * the team repo.
33
34
  *
34
35
  * ClickUp guard (any stage, any tool name containing "clickup"): an ALLOWLIST. Reads pass.
35
- * Writes pass only for `mcp__clickup__clickup_update_task` (allowed keys and fields only) and
36
+ * Writes pass only for `mcp__clickup__clickup_update_task` (allowed keys and fields only; status
37
+ * only "in progress" or "development finished") and
36
38
  * `mcp__clickup__clickup_create_comment` (entity_type "task"), on the session ticket. Everything
37
39
  * else is blocked, including every tool of the old "ClickUP for Dev" server. This branch FAILS
38
40
  * CLOSED on an internal error; the rest of the gate fails open.
@@ -101,7 +103,8 @@ const OLD_CLICKUP_SERVER_RE = /^claude_ai_ClickUP_for_Dev$/i;
101
103
  const CLICKUP_READ_RE = /^(get_.+|search|search_.+|filter_.+|list_.+|find_.+|resolve_assignees)$/;
102
104
  const UPDATE_TASK_KEYS = new Set(['task_id', 'workspace_id', 'status', 'name', 'custom_fields']);
103
105
  const CUSTOM_FIELD_ITEM_KEYS = new Set(['id', 'value']);
104
- const DONE_STATUS = 'development finished';
106
+ /* /start sets "in progress" (only from "to do"); /finish sets "development finished". */
107
+ const ALLOWED_STATUSES = new Set(['in progress', 'development finished']);
105
108
  const WRITABLE_FIELDS = new Map([
106
109
  ['32e9fcd3-6f59-448a-a06e-de52afc87587', 'Application'],
107
110
  ['ec15edfd-5920-4c12-a1dc-94ede4a62438', 'Stakeholders'],
@@ -184,7 +187,7 @@ const MSG = {
184
187
  alwaysBlocked: (op) => 'ClickUp call blocked: `' + op + '` is not allowed from Claude. Ask the developer to do it in ClickUp.',
185
188
  commentEntity: 'ClickUp write blocked: comments may only go on the session ticket (entity_type "task").',
186
189
  badKey: (k) => 'ClickUp write blocked: update_task may not set `' + k + '`. Allowed: status, name, custom_fields (Application, Stakeholders, Task Type, Pseudocode, Has UI Changes).',
187
- badStatus: 'ClickUp write blocked: the only status Claude may set is "development finished".',
190
+ badStatus: 'ClickUp write blocked: the only statuses Claude may set are "in progress" (/start) and "development finished" (/finish).',
188
191
  hasCodeChanges: 'ClickUp write blocked: never write Has Code Changes — worker2 owns it.',
189
192
  badField: (id) => 'ClickUp write blocked: custom field `' + id + '` is not one Claude may write.',
190
193
  guardError: 'ClickUp call blocked: the start gate could not check it (internal error). Try again; if it repeats, tell the developer.',
@@ -610,6 +613,7 @@ function isGhRead(words) {
610
613
  if (!words.length || words[0].toLowerCase() !== 'gh') return false;
611
614
  const a = (words[1] || '').toLowerCase();
612
615
  const b = (words[2] || '').toLowerCase();
616
+ if (a === 'repo' && b === 'view') return !words.slice(3).some((w) => /^(-w|--web)$/i.test(w)); // default branch lookup for create-branch
613
617
  return (a === 'auth' && b === 'status') || (a === 'pr' && ['list', 'view', 'checks'].includes(b));
614
618
  }
615
619
 
@@ -768,7 +772,7 @@ function checkCustomFields(value) {
768
772
 
769
773
  function checkUpdateTask(ti) {
770
774
  for (const k of Object.keys(ti)) if (!UPDATE_TASK_KEYS.has(k)) return MSG.badKey(k);
771
- if ('status' in ti && (typeof ti.status !== 'string' || ti.status.trim().toLowerCase() !== DONE_STATUS)) return MSG.badStatus;
775
+ if ('status' in ti && (typeof ti.status !== 'string' || !ALLOWED_STATUSES.has(ti.status.trim().toLowerCase()))) return MSG.badStatus;
772
776
  if ('name' in ti && typeof ti.name !== 'string') return MSG.badKey('name');
773
777
  if ('custom_fields' in ti) return checkCustomFields(ti.custom_fields);
774
778
  return null;
@@ -439,6 +439,7 @@ const RETIRED_TOGA_HOOKS = ['kickoff-gate.js'];
439
439
  const RETIRED_TOGA_PERMISSIONS = { ask: [
440
440
  'mcp__clickup__clickup_create_task_comment',
441
441
  'mcp__clickup__clickup_create_comment',
442
+ 'mcp__clickup__clickup_update_task',
442
443
  ] };
443
444
 
444
445
  function mergeSettings(claudeDir, sourceDir) {
@@ -25,9 +25,10 @@ The hook already checked the id, wrote the session ticket file
25
25
  `node "<TEAM_REPO>/knowledge.js" kickoff-primed`.
26
26
  3. **open** — normal work. ClickUp writes still only go to this ticket, only with
27
27
  `mcp__clickup__clickup_update_task` (fields: Application, Stakeholders, Task Type,
28
- Pseudocode, Has UI Changes, `name`, status `development finished`) and
29
- `mcp__clickup__clickup_create_comment`. The hook blocks every other ClickUp write, and
30
- Claude Code asks the developer before each one. /start itself writes nothing to ClickUp.
28
+ Pseudocode, Has UI Changes, `name`, status `in progress` / `development finished`) and
29
+ `mcp__clickup__clickup_create_comment`. The hook blocks every other ClickUp write. Field
30
+ changes are written only after the developer says yes in chat; status changes need no
31
+ question. /start writes only the status `to do` -> `in progress` (Step 6b).
31
32
 
32
33
  Trailing text is never permission to skip steps. If a block message appears, do the step it
33
34
  names. Do not fight the gate. (Harness work only: `TOGA_START_GATE_DISABLED=1`.)
@@ -93,20 +94,59 @@ customId is not the session ticket, the session file is missing, the internal id
93
94
  or the url is not `https://app.clickup.com/...`. On a refusal, show the error and stop. On
94
95
  `{ok:true,...}` the gate moves to the primer stage.
95
96
 
96
- ## Step 4 — Branch check
97
-
98
- ```
99
- node ".../start-helper.js" branch-check --session <sid> --root "$CLAUDE_PROJECT_DIR"
100
- ```
101
-
102
- Prints `{matches:[{repo,branch}], mismatches:[{repo,branch,changes}], notInvolved:[...],
103
- skipped:[...]}`. The KB repo (toga-tech) is skipped — it works on `_main`. A repo on another
104
- branch with no tracked changes is "not involved", not a problem.
105
-
106
- If `mismatches` is not empty, STOP and show a numbered list: repo, current branch, number of
107
- changed files, expected branch `<T>`. Say: "Switch these repos to `<T>` (ClickUp creates the
108
- branch), then tell me. Or say 'continue anyway'." Never switch branches yourself. Only on an
109
- explicit "continue anyway":
97
+ ## Step 4 — Ticket repos and branches
98
+
99
+ 1. **Repo list from the ticket's `application` values:**
100
+
101
+ | Application | Repo(s) |
102
+ |---|---|
103
+ | TOGa Supply | `toga25-supply` (+ `api2` if backend work) |
104
+ | TOGa Desk | `toga25-desk` |
105
+ | TOGa IQ | `toga25-iq` |
106
+ | TOGa Commerce | `toga2-commerce` |
107
+ | TOGa Blox | `toga25-blox` |
108
+ | TOGa Platform | `toga2-template` |
109
+ | API2 | `api2` |
110
+ | Worker 2.0 | `worker2` |
111
+ | _underscore | `_underscore` |
112
+ | Library | `library` |
113
+ | Worker 1.0 | `worker` |
114
+ | TOGa Classic 1.0 | `toga` |
115
+ | Team Knowledge Base | the KB repo only — it works on `_main`, no branch work |
116
+ | anything else | ask the developer |
117
+
118
+ Show the list. The developer can add or drop repos. KB-only ticket → skip the rest of Step 4.
119
+
120
+ 2. **Check only those repos:**
121
+ ```
122
+ node ".../start-helper.js" branch-check --session <sid> --root "$CLAUDE_PROJECT_DIR" --repos <a,b>
123
+ ```
124
+ Prints `{matches, mismatches:[{repo,branch,changes}], missing, skipped}`. Repos not listed are
125
+ ignored (other work in them is not this ticket's problem). `missing` = no clone at that path
126
+ — ask for the local path.
127
+
128
+ 3. **For each repo in `mismatches`, ask once:** "Create/switch to `<T>` in `<repo>` from
129
+ `<base>`?" Resolve `<base>` with
130
+ `gh repo view <owner/repo> --json defaultBranchRef -q .defaultBranchRef.name` when gh works
131
+ (owner/repo from `git -C <repo> remote get-url origin`); else leave `--base` off (origin's
132
+ default is used). If `changes` > 0, do not offer — STOP and say: "`<repo>` has uncommitted
133
+ changes on `<branch>`. Commit or move them, then tell me." Never stash.
134
+
135
+ 4. **On yes:**
136
+ ```
137
+ node ".../start-helper.js" create-branch --session <sid> --repo "<repo path>" --base <base>
138
+ ```
139
+ It checks out `origin/<T>` if it exists, else local `<T>`, else creates `<T>` from
140
+ `origin/<base>` and pushes it (`git push -u origin <T>`, never force). It refuses on
141
+ uncommitted tracked changes and on the KB repo. Prints
142
+ `{ok, repo, branch, base, created, pushed, from}`. On `ok:false`, show `error` and stop.
143
+ Then run `branch-check --repos` again, fresh, and report what it shows.
144
+ Branches named with the ticket id link to the ClickUp ticket through the GitHub
145
+ integration — on the first real run, confirm the link shows on the ticket.
146
+
147
+ This is the **only** time Claude creates or switches a branch, and only after the developer's
148
+ yes (`rules/common/git-workflow.md`). On no, say: "Switch these repos to `<T>`, then tell me.
149
+ Or say 'continue anyway'." Only on an explicit "continue anyway":
110
150
 
111
151
  ```
112
152
  node ".../start-helper.js" override-branch --session <sid>
@@ -159,6 +199,21 @@ Same method as the old /kickoff. Do it in this main session, no agents.
159
199
 
160
200
  If `estimate.heavy` is true, suggest dropping repos the ticket does not touch.
161
201
 
202
+ ## Step 6b — Status to "in progress"
203
+
204
+ After verify and priming: if the reader's `status` is `to do` (any case), set it — no question:
205
+
206
+ ```
207
+ mcp__clickup__clickup_update_task
208
+ task_id: "<T>"
209
+ status: "in progress"
210
+ ```
211
+
212
+ Tell the developer in one line: "Ticket <T> moved to in progress." Any other status (in
213
+ progress, review, rework, development finished, ...) → leave it; never move a ticket backward.
214
+ If ClickUp rejects the status (the list has no "in progress"), say so in one line and skip.
215
+ Status flow: to do -> in progress (/start) -> development finished (/finish).
216
+
162
217
  ## Step 7 — Pseudocode and rework
163
218
 
164
219
  - **`pseudocode`** not empty -> show it and follow it as the agreed plan. If the work needs to differ,
@@ -179,7 +234,8 @@ Ticket: <T> — <title> (<status>, <list>)
179
234
  Apps: <repos> (core: <core>)
180
235
  Clients: <clients>
181
236
  Task type: <type>
182
- Branch: all on <T> | override | mismatches listed
237
+ Status: <old> -> in progress | unchanged
238
+ Branch: all on <T> | created/switched: <repos> | override | mismatches listed
183
239
  Pseudocode: yes | no
184
240
  Rework: yes (PR #n) | no
185
241
  KB loaded: <doc titles>; gaps: <none | list>
@@ -20,14 +20,24 @@
20
20
  * scan --file <path> secret scan: exit 1 + {ok:false,hits:[types]}
21
21
  * on a match, exit 0 + {ok:true} if clean,
22
22
  * exit 2 if the file can't be read
23
- * branch-check --session <sid> --root <dir> {ok, ticket, matches, mismatches, notInvolved, skipped}
23
+ * branch-check --session <sid> --root <dir> [--repos <a,b>]
24
+ * {ok, ticket, matches, mismatches, notInvolved, missing, skipped}
25
+ * --repos: comma list of repo folder names (under --root) or paths. Only those
26
+ * repos are checked (match / mismatch / missing); every other repo is ignored.
27
+ * create-branch --session <sid> --repo <path> [--base <branch>]
28
+ * Checks out the ticket branch (name = the verified session ticket). Order:
29
+ * origin/<T> exists -> check it out (tracking); else local <T> -> check it out;
30
+ * else create <T> from origin/<base> and `git push -u origin <T>` (never force).
31
+ * Refuses on tracked uncommitted changes (never stashes) and on the KB repo.
32
+ * {ok, repo, branch, base, created, pushed, from}
24
33
  * override-branch --session <sid> branchOverride -> true
25
34
  * latest-for --ticket <T> [--max-age-hours 24] [--exclude-session <sid>]
26
35
  * newest verified file for that ticket, or {}
27
36
  * repos-with-work --session <sid> --root <dir> {ok, ticket, repos:[...]}
28
37
  *
29
38
  * Exit 0 on success, 1 on error (prints {ok:false, error}). Prunes files older than 30 days on
30
- * every write. Never pushes, fetches, or changes a branch.
39
+ * every write. Only create-branch fetches, pushes, or changes a branch — and /start runs it only
40
+ * after the developer says yes.
31
41
  */
32
42
 
33
43
  const fs = require('fs');
@@ -49,6 +59,8 @@ const MS_PER_DAY = 24 * 60 * 60 * 1000;
49
59
  const MS_PER_HOUR = 60 * 60 * 1000;
50
60
  const DEFAULT_REATTACH_HOURS = 24;
51
61
  const GIT_TIMEOUT_MS = 20000;
62
+ const GIT_NETWORK_TIMEOUT_MS = 120000;
63
+ const MAX_ERROR_CHARS = 500;
52
64
  const TICKET_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
53
65
  const KB_REMOTE_RE = /(agilantsolutions|togatech)\/claude(\.git)?\/?$/i;
54
66
  const DEFAULT_BRANCH_GUESSES = ['_production', 'main', 'master', '_main'];
@@ -159,6 +171,19 @@ function git(repo, args) {
159
171
  }
160
172
  }
161
173
 
174
+ /* Like git(), but throws an Error carrying git's stderr. Used where the failure reason matters. */
175
+ function gitOrThrow(repo, args, timeout) {
176
+ try {
177
+ return execFileSync('git', ['-C', repo].concat(args), {
178
+ encoding: 'utf8', timeout: timeout || GIT_TIMEOUT_MS, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
179
+ env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: '0' }),
180
+ }).trim();
181
+ } catch (e) {
182
+ const detail = String((e.stderr && e.stderr.toString()) || e.message || e).trim().slice(0, MAX_ERROR_CHARS);
183
+ throw new Error('git ' + args.join(' ') + ' failed: ' + detail);
184
+ }
185
+ }
186
+
162
187
  function isRepo(dir) {
163
188
  try { return fs.existsSync(path.join(dir, '.git')); } catch (e) { return false; }
164
189
  }
@@ -195,10 +220,14 @@ function currentBranch(repo) {
195
220
  return b === 'HEAD' ? '(detached)' : b;
196
221
  }
197
222
 
198
- function trackedChangeCount(repo) {
223
+ function trackedChanges(repo) {
199
224
  const out = git(repo, ['status', '--porcelain', '--untracked-files=no']);
200
- if (out === null) return 0;
201
- return out ? out.split(/\r?\n/).filter(Boolean).length : 0;
225
+ if (!out) return [];
226
+ return out.split(/\r?\n/).filter(Boolean);
227
+ }
228
+
229
+ function trackedChangeCount(repo) {
230
+ return trackedChanges(repo).length;
202
231
  }
203
232
 
204
233
  function localBranchFor(repo, ticket) {
@@ -217,6 +246,21 @@ function defaultRemoteBranch(repo) {
217
246
  return DEFAULT_BRANCH_GUESSES.find((b) => refExists(repo, 'refs/remotes/origin/' + b)) || null;
218
247
  }
219
248
 
249
+ /* origin's default branch: origin/HEAD, else "HEAD branch:" from `git remote show origin`. */
250
+ function originDefaultBranch(repo) {
251
+ const head = git(repo, ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD']);
252
+ if (head) return head.replace(/^refs\/remotes\/origin\//, '');
253
+ let shown = null;
254
+ try { shown = gitOrThrow(repo, ['remote', 'show', 'origin'], GIT_NETWORK_TIMEOUT_MS); } catch (e) { return null; }
255
+ const m = shown.match(/HEAD branch:\s*(\S+)/);
256
+ return m && m[1] !== '(unknown)' ? m[1] : null;
257
+ }
258
+
259
+ function validBranchName(repo, name) {
260
+ return typeof name === 'string' && !!name && !name.startsWith('-') &&
261
+ git(repo, ['check-ref-format', '--branch', name]) !== null;
262
+ }
263
+
220
264
  function aheadCount(repo, base, branch) {
221
265
  const n = git(repo, ['rev-list', '--count', base + '..' + branch]);
222
266
  return n === null ? null : parseInt(n, 10);
@@ -338,12 +382,35 @@ function cmdLatestFor(args) {
338
382
  print(best || {});
339
383
  }
340
384
 
385
+ /* --repos "a,b,D:/x/c": folder names resolve under root; paths are used as given. */
386
+ function listedRepos(root, list) {
387
+ return String(list).split(',').map((x) => x.trim()).filter(Boolean)
388
+ .map((x) => (path.isAbsolute(x) || /[\/\\]/.test(x) ? path.resolve(x) : path.join(path.resolve(root), x)));
389
+ }
390
+
391
+ /* Listed repos only: each is a match, a mismatch (any other branch, changes or not) or missing. */
392
+ function checkListedRepos(result, repos, ticket) {
393
+ for (const repo of repos) {
394
+ if (!isRepo(repo)) { result.missing.push({ repo }); continue; }
395
+ if (isKbRepo(repo)) { result.skipped.push({ repo, reason: 'KB repo (works on _main)' }); continue; }
396
+ const branch = currentBranch(repo);
397
+ if (branch === null) { result.skipped.push({ repo, reason: 'git failed' }); continue; }
398
+ if (branch.toLowerCase() === ticket.toLowerCase()) { result.matches.push({ repo, branch }); continue; }
399
+ result.mismatches.push({ repo, branch, changes: trackedChangeCount(repo) });
400
+ }
401
+ }
402
+
341
403
  function cmdBranchCheck(args) {
342
404
  const r = requireSession(args);
343
405
  if (!r) return;
344
406
  if (typeof args.root !== 'string') return fail('missing --root <dir>');
345
407
  const ticket = r.s.ticket;
346
- const result = { ok: true, ticket, branchOverride: !!r.s.branchOverride, matches: [], mismatches: [], notInvolved: [], skipped: [] };
408
+ const result = { ok: true, ticket, branchOverride: !!r.s.branchOverride, matches: [], mismatches: [], notInvolved: [], missing: [], skipped: [] };
409
+ if (args.repos !== undefined) {
410
+ if (typeof args.repos !== 'string' || !args.repos.trim()) return fail('--repos needs a comma list of repo names or paths');
411
+ checkListedRepos(result, listedRepos(args.root, args.repos), ticket);
412
+ return print(result);
413
+ }
347
414
  for (const repo of reposUnder(args.root)) {
348
415
  if (isKbRepo(repo)) { result.skipped.push({ repo, reason: 'KB repo (works on _main)' }); continue; }
349
416
  const branch = currentBranch(repo);
@@ -356,6 +423,73 @@ function cmdBranchCheck(args) {
356
423
  print(result);
357
424
  }
358
425
 
426
+ function createBranchError(repo, message, extra) {
427
+ print(Object.assign({ ok: false, repo, error: message }, extra || {}));
428
+ process.exitCode = 1;
429
+ }
430
+
431
+ /* Base for a new branch: --base, else origin's default. */
432
+ function resolveBase(repo, given) {
433
+ if (given !== undefined) {
434
+ if (!validBranchName(repo, given)) return { error: 'bad --base branch name' };
435
+ return { base: given };
436
+ }
437
+ const base = originDefaultBranch(repo);
438
+ return base ? { base } : { error: 'cannot find origin\'s default branch — pass --base <branch>' };
439
+ }
440
+
441
+ /* origin/<T> first, then local <T>, else create from origin/<base> and push (never force). */
442
+ function checkoutTicket(repo, ticket, base) {
443
+ const onRemote = refExists(repo, 'refs/remotes/origin/' + ticket);
444
+ const onLocal = refExists(repo, 'refs/heads/' + ticket);
445
+ if (onRemote) {
446
+ if (onLocal) gitOrThrow(repo, ['checkout', ticket]);
447
+ else gitOrThrow(repo, ['checkout', '-b', ticket, '--track', 'origin/' + ticket]);
448
+ return { created: false, pushed: false, from: onLocal ? 'local' : 'origin/' + ticket };
449
+ }
450
+ if (onLocal) {
451
+ gitOrThrow(repo, ['checkout', ticket]);
452
+ return { created: false, pushed: false, from: 'local' };
453
+ }
454
+ if (!base.base) return { error: base.error };
455
+ if (!refExists(repo, 'refs/remotes/origin/' + base.base)) return { error: 'origin/' + base.base + ' does not exist' };
456
+ gitOrThrow(repo, ['checkout', '--no-track', '-b', ticket, 'origin/' + base.base]);
457
+ try {
458
+ gitOrThrow(repo, ['push', '-u', 'origin', ticket], GIT_NETWORK_TIMEOUT_MS);
459
+ } catch (e) {
460
+ return { error: 'branch created locally but the push failed: ' + e.message, created: true, pushed: false };
461
+ }
462
+ return { created: true, pushed: true, from: 'origin/' + base.base };
463
+ }
464
+
465
+ function cmdCreateBranch(args) {
466
+ const r = requireSession(args);
467
+ if (!r) return;
468
+ if (r.s.state !== 'verified') return fail('session ticket is not verified — finish /start verify first');
469
+ const ticket = String(r.s.ticket || '');
470
+ if (!TICKET_RE.test(ticket)) return fail('bad session ticket');
471
+ if (typeof args.repo !== 'string' || !args.repo) return fail('missing --repo <path>');
472
+ const repo = path.resolve(args.repo);
473
+ if (!isRepo(repo)) return createBranchError(repo, 'not a git repo');
474
+ if (isKbRepo(repo)) return createBranchError(repo, 'KB repo works on _main — no ticket branch');
475
+ if (args.base !== undefined && typeof args.base !== 'string') return createBranchError(repo, '--base needs a branch name');
476
+ if (currentBranch(repo) === ticket) {
477
+ return print({ ok: true, repo, branch: ticket, base: args.base || null, created: false, pushed: false, from: 'current' });
478
+ }
479
+ const dirty = trackedChanges(repo);
480
+ if (dirty.length) return createBranchError(repo, 'uncommitted tracked changes — commit or move them first (never stashed)', { changes: dirty });
481
+ try {
482
+ gitOrThrow(repo, ['fetch', 'origin'], GIT_NETWORK_TIMEOUT_MS);
483
+ } catch (e) {
484
+ return createBranchError(repo, e.message);
485
+ }
486
+ const base = resolveBase(repo, args.base);
487
+ let out;
488
+ try { out = checkoutTicket(repo, ticket, base); } catch (e) { return createBranchError(repo, e.message); }
489
+ if (out.error) return createBranchError(repo, out.error, out.created ? { branch: ticket, created: true, pushed: false } : null);
490
+ print(Object.assign({ ok: true, repo, branch: currentBranch(repo), base: base.base || null }, out));
491
+ }
492
+
359
493
  function cmdReposWithWork(args) {
360
494
  const r = requireSession(args);
361
495
  if (!r) return;
@@ -389,9 +523,10 @@ function main() {
389
523
  case 'override-branch': return cmdOverrideBranch(args);
390
524
  case 'latest-for': return cmdLatestFor(args);
391
525
  case 'repos-with-work': return cmdReposWithWork(args);
526
+ case 'create-branch': return cmdCreateBranch(args);
392
527
  case 'scan': return cmdScan(args);
393
528
  default:
394
- return fail('usage: start-helper.js <get|verify|branch-check|override-branch|latest-for|repos-with-work|scan> [--flags]');
529
+ return fail('usage: start-helper.js <get|verify|branch-check|create-branch|override-branch|latest-for|repos-with-work|scan> [--flags]');
395
530
  }
396
531
  }
397
532