toga-ai 1.0.1024 → 1.0.1026

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -20,7 +20,7 @@ as `/skill-name` inside Claude Code.
20
20
  |-------|---------|-------------|
21
21
  | **start** | `/start TRUE-1234` | **Start of every session.** ClickUp ticket id only. Reads the ticket (apps, clients, task type, pseudocode), checks branches, then primes the framework architecture, coding standards, repo knowledge, and client context. Edits are blocked until it runs. |
22
22
  | **commit** | `/commit` | Commit only the files this session changed, with the session notes in the message, then pushes the ticket branch. |
23
- | **pseudocode** | `/pseudocode` | Major tickets only. Writes the agreed approach as plain text to the ticket's Pseudocode field, after approval. |
23
+ | **pseudocode** | `/pseudocode` | Major tickets only. Writes the agreed approach as plain text to the ticket's Pseudocode field (no separate write approval). |
24
24
  | **finish** | `/finish` | **End of every session.** Records what was built or changed into the knowledge base, checks the ticket fields, and when the ticket is done pushes, opens the PRs, comments on the ticket, and sets it to development finished. |
25
25
  | **toga-loop** | `/toga-loop` | Orchestrate a bigger or iterative change (multi-file feature, refactor, migration, sweep, or "keep going until X") as an explore→implement→verify loop driven by subagents, with progress tracked on a state file so it survives compaction and can resume. Run after `/start`. |
26
26
  | **feature** | `/feature` | Build a feature end-to-end as a subagent-driven loop: restate goal + done-condition, get an independent second opinion on the approach before coding, plan, implement (worktree-isolated for parallel work), verify with reviewers, loop until done, hand off to `/finish`. Run after `/start`. |
@@ -14,6 +14,7 @@
14
14
  | [Calculated-column filter options (`<field>FilterOptions` model static → table meta)](features/calculated-column-filter-options.md) | Lets a calculated (`FIELD_SQL`) table column supply its own filter dropdown options via a `<field>FilterOptions` model static; open when adding a data-driven co |
15
15
  | [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent |
16
16
  | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind Fulfill & Ship: buying/persisting a carrier label (UPS/FedEx) and creating the NetSuite Item Fulfillment; open for label, reprint, retu |
17
+ | [Case status is derived from stage/owner; stage changes pass a reason-note gate](features/case-status-and-stage-gate.md) | `_Model_Client_Case::_status` is DERIVED (never set) and stage writes via api2 are gated by a `case-set-stage` Surface rule; open before writing Cases from code |
17
18
  | [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | Minimum recipe + traps for running real 2.0 framework code (`_Model`/`_Query`/`_Database`) from a bare `php script.php`; open before writing a CLI harness/backf |
18
19
  | [Client Users require a contactId — the postPost backfill interceptor & every creation path](features/client-user-contactid-required.md) | Every `Client_<Tenant>.Users` row must have a non-null `contactId`; the shared `_Model_Client_User::postPost` interceptor guarantees it for api2-created users, |
19
20
  | [_Cloud credentials (WorkloadsRuntime) + S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` credentials (assumes `WorkloadsRuntime`, no IAM keys) and S3 helpers (copy/get/delete/list, per-bucket region, 5 GB single-PUT limit); open before any |
@@ -0,0 +1,46 @@
1
+ ---
2
+ title: "Case status is derived from stage/owner; stage changes pass a reason-note gate"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-15
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Client/Case.php
13
+ - _underscore/Model/Client/CaseNote.php
14
+ - _underscore/Model/Client/CaseStage.php
15
+ - _underscore/Model/Client/Ticket.php
16
+ related:
17
+ - ../../api2/features/api-payload-interceptors.md
18
+ - ./note-record-authorship.md
19
+ - ../../../../clients/nycdoe/features/asn-sftp-ingestion-2-0.md
20
+ ---
21
+
22
+ `_Model_Client_Case::_status` is DERIVED (never set) and stage writes via api2 are gated by a `case-set-stage` Surface rule; open before writing Cases from code, an integration, or the API.
23
+
24
+ ## Summary
25
+
26
+ Stages are set; statuses are derived. A case's `_status` slug comes from its stage, else its owner. Moving a case to a blocker stage needs a stored reason note in the same request. Built for TOGa Desk Case Blocker (TRUE-82615, Desk ADR 0144). Shared core: applies to every 2.0 client. **On `_underscore` branch `TRUE-82615` (toga25 line) as of 2026-10-15 — not on `_production`.**
27
+
28
+ ## How it works
29
+
30
+ - **`_status`** (plain `FIELD_SQL`, text slug): stage's linked `CaseStatuses.slug` if `caseStageId` set → else `ownerUserId` set → `in-progress` → else `open`. Same rule as `_Model_Client_Ticket::_status`. Never add `FIELDOPT_SQL_TYPE` (a cast zeroes the slug).
31
+ - **Blocker stages** (`roadblocked`, `on-hold`, `awaiting-user`) are `CaseStages` rows keyed by the new `CaseStages.slug`. Resume = set `caseStageId` to NULL (status falls back to owner/open).
32
+ - **`Client.CaseNotes` / `_Model_Client_CaseNote`**: write-once reason notes (`caseId`, `caseStageId`, `note`). `authorUserId` is stamped in `::save()` from `_Model::getRequestUserId()`, never from the payload. ACL: create + read only.
33
+ - **Set-stage gate**: Case `prePut`/`prePatch` capture the before-stage; `postPut`/`postPatch`/`postPost` read the `case-set-stage` Surface `requiredRule` for the new stage slug and its status slug. If the rule needs a reason, a `CaseNotes` row for this case + stage must have been stored in this request (`CaseNote::countCreatedFor`) — else `_Exception_Validation` (400) and the whole request rolls back. Wired by 5 `Core.ApiPayloadInterceptors` rows on cases (recordId 222).
34
+ - **`Ticket::createCase`** (`{"createCase":{}}` on PUT /tickets/{uuid}) now creates the case with **no stage** (it used to force the Open stage), so status derives.
35
+ - Central ids: `Core.Records` 377 (case-notes); `Core.RecordFields` 2743-2750. SQL: dbchanges2 `Client/2026-10-14b`, `Client/2026-10-14c`, `Core/2026-10-14b`, `Client/2026-10-15a` (case-notes ACL), `Client/2026-10-15b` (cases-stages read ACL).
36
+
37
+ ## Gotchas
38
+
39
+ - **Do not write `_status` or pick an "Open" stage to mean open.** Leave `caseStageId` NULL; status derives.
40
+ - **Direct `_Model` writes skip the gate** (interceptors run only in api2). Nycdoe SFTP `Parse.php` creates cases this way; its cases now read `open` / `in-progress`.
41
+ - **PUT/PATCH a case without a route uuid → 400** — the gate needs `$api->recordUuid`.
42
+ - **Keep Cases FK child policies `MATCH`.** `MATCH_UPDATE` / `UPSERT` / `CREATE` write a case through a parent's FK object and bypass the gate.
43
+ - **The reason must be a stored row, not payload text** — api2 can swallow a failed nested note and still return 200.
44
+
45
+ ## Change history
46
+ - 2026-10-15 — Created: derived case status, blocker stages, CaseNotes, set-stage gate, createCase without stage (TRUE-82615). (jcardinal)
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [apeterson]
9
+ updated: 2026-10-15
10
+ owners: [apeterson, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/SalesOrderNote.php
13
13
  - _underscore/Model/Client/PurchaseOrderNote.php
@@ -18,6 +18,7 @@ files:
18
18
  - _underscore/Model/Client/PartRequestNote.php
19
19
  related:
20
20
  - ../../toga-blox/features/add-notes-template.md
21
+ - ./case-status-and-stage-gate.md
21
22
  - ../../toga25-supply/features/transfer-orders-page.md
22
23
  - ../features/sales-order-denial-reason.md
23
24
  - ../../../clients/compass-usa/features/approval-decision-flow.md
@@ -62,8 +63,12 @@ Before building anything that shows or filters by note author:
62
63
 
63
64
  - **`contactId` on a `*Notes` model is an unwritten FK — treat it as always NULL.** Seven models, zero writers.
64
65
  - **A notes list UI has no author to show.** Design for it, or the field silently renders blank for every row.
66
+ - **Exception — `CaseNotes` (2026-10-15) has a real author:** `authorUserId` stamped in `_Model_Client_CaseNote::save()` from `_Model::getRequestUserId()`. Copy that pattern for the real fix. See [Case status + stage gate](./case-status-and-stage-gate.md).
65
67
  - **`dtStamp` is `FIELD_DATETIME_CREATED`** — stamped by the ORM on insert, the only provenance a note actually carries.
66
68
 
69
+ ## Change history
70
+ - 2026-10-15 — Noted CaseNotes as the first note model with a server-stamped author. (jcardinal)
71
+
67
72
  ## Related
68
73
 
69
74
  - [Add Notes template](../../toga-blox/features/add-notes-template.md)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-08
10
- owners: [dfranks]
9
+ updated: 2026-10-15
10
+ owners: [dfranks, jcardinal]
11
11
  files: []
12
12
  related:
13
13
  - ../../../../standalone/apps/claude/workflows/clickup-ticket-workflow.md
@@ -24,7 +24,7 @@ How ClickUp surfaces branches/PRs/commits in a ticket's GitHub tab and the branc
24
24
 
25
25
  - ClickUp's **native GitHub integration** auto-surfaces a branch, PR, or commit in a ticket's GitHub tab **whenever the ticket id appears in the branch name and/or the PR title/body**. Match is on the textual ticket id (e.g. `TRUE-79868`).
26
26
  - **No ClickUp API call creates this link.** Linking is purely a side effect of the ticket id appearing in Git metadata.
27
- - Durable rule for ticket work: name the **git branch as the BARE ticket id** (e.g. `TRUE-79868`), **one branch per repo**, PR title starts with the ticket id (`TRUE-79868 - <subject>`). PR base = the repo's default branch (usually `_production`, but it varies — resolve it with `gh repo view`). Confirmed working 2026-10-08.
27
+ - Durable rule for ticket work: name the **git branch as the BARE ticket id** (e.g. `TRUE-79868`), **one branch per repo**, PR title starts with the ticket id (`TRUE-79868 - <subject>`). PR base is set by the ticket's project — see `rules/common/git-workflow.md` → *Base branch*. Confirmed working 2026-10-08.
28
28
 
29
29
  ## Gotchas
30
30
 
@@ -38,6 +38,7 @@ How ClickUp surfaces branches/PRs/commits in a ticket's GitHub tab and the branc
38
38
 
39
39
  ## Change history
40
40
 
41
+ - 2026-10-15 — PR base now follows the ticket's project (Desk/IQ → `toga25`); links to git-workflow.md instead of restating. (jcardinal)
41
42
  - 2026-10-08 — Added reconnect fix, Development-space repo linking, and settings to leave alone. (jcardinal)
42
43
  - 2026-10-08 — `/work-ticket` and `/plan-ticket` retired; `/finish` opens the PRs. PR title = ticket id first; base = repo default branch; git-workflow now uses bare ticket-id branches. (jcardinal)
43
44
 
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
21
21
 
22
22
  ## 2.0 framework
23
23
 
24
- - **_underscore** (_Underscore) _(framework core)_ — 95 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
24
+ - **_underscore** (_Underscore) _(framework core)_ — 96 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
25
25
  - **worker2** (Worker) — 86 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
26
26
  - **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
27
27
  - **dbchanges2** (Database Changes) _(framework core)_ — 22 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: nycdoe
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-10-06
10
- owners: [mhammontree]
9
+ updated: 2026-10-15
10
+ owners: [mhammontree, jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Sftp/Import.php
13
13
  - worker2/Worker/Sftp/Parse.php
@@ -23,6 +23,7 @@ related:
23
23
  - ../profile.md
24
24
  - ../../../2.0/apps/worker2/features/codeception-integration-tests.md
25
25
  - ../../../2.0/apps/_underscore/features/cli-script-bootstrap.md
26
+ - ../../../2.0/apps/_underscore/features/case-status-and-stage-gate.md
26
27
  ---
27
28
 
28
29
  2.0 rewrite of DOE's vendor-SFTP ASN intake (TRUE-80519 Phases 1–2): per-manufacturer fetch to `Files`, header-validated parse into one Case per customer PO + one ServiceRequest per line; open for any 2.0 DOE ASN/SFTP work.
@@ -112,8 +113,10 @@ PRs open 2026-10-06: library, worker2, dbchanges2, _underscore (worker optional
112
113
  - **`connect()` must not chain the phpseclib exception** — phpseclib `login()` frames carry the first 15 chars of the password, and worker2 stores job traces in `Core.WorkerJobs`. It rethrows a fresh exception with no `previous`.
113
114
  - **Locks and just-committed rows must read the writer** — see the read-host gotcha in [Per-Client Database Connections](../../../2.0/apps/_underscore/features/per-client-database-connections.md).
114
115
  - **Schema drift: `_Model_Client_Case` declares `longDescription` but `Client_Nycdoe.Cases` lacked the column** → any direct-model Case load/save 1054s. Fixed for DOE (Client_Nycdoe schema file). Other tenants' `Cases` may have the same gap — unchecked (ask Rohan).
116
+ - **Parsed Cases get no stage; `_status` is derived** (`open`, or `in-progress` once an owner is set) since TRUE-82615. Direct model writes skip the api2 set-stage gate. Never set a stage to mean "open" — see [Case status + stage gate](../../../2.0/apps/_underscore/features/case-status-and-stage-gate.md).
115
117
 
116
118
  ## Change history
119
+ - 2026-10-15 — Gotcha: parsed Cases now derive status (no stage) after the shared Case model change. (jcardinal)
117
120
  - 2026-10-06 — 9 dbchanges2 files merged into one Client_Nycdoe + one Core file; runbook Part A updated (mhammontree)
118
121
  - 2026-10-06 — Activation/deployment runbook (Part A inert, Part B shadow mode, rollback, failed-file release) (mhammontree)
119
122
  - 2026-10-06 — Gated delete built (RunForFile/DeleteSource, Parse claim + status-outside-transaction fix, retired names, `released` status); open cutover items (mhammontree)
@@ -6,7 +6,7 @@ project: Claude Harness
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-13
9
+ updated: 2026-10-15
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - claude/skills/start/SKILL.md
@@ -44,10 +44,11 @@ writes, and agents until the session has a ticket. These replaced `/kickoff`, `/
44
44
  exists (rework: same branch, same PR). If ClickUp can't be read, it stops — no offline mode.
45
45
  2. **`/commit`** — commits only this session's files, ticket from the session file, then the
46
46
  branch. Then pushes the ticket branch (`git push -u origin <TICKET>`, never force).
47
- 3. **`/pseudocode`** — major tickets only. Writes the agreed approach to the Pseudocode field
48
- after the developer approves. Touches no other field.
49
- 4. **`/finish`** — always writes the team KB first. Then one approval list for ticket fields
50
- (Application, Stakeholders, Task Type, title, Has UI Changes). Then it asks "Is this ticket finished?" — a yes is the permission (no extra asks): commit any
47
+ 3. **`/pseudocode`** — major tickets only. Writes the approach agreed with the developer to the
48
+ Pseudocode field — no separate write approval. Touches no other field.
49
+ 4. **`/finish`** — always writes the team KB first. Then Claude decides and writes the ticket
50
+ fields itself in one call (Application, Stakeholders, Task Type, title, Has UI Changes) and
51
+ reports before → after — no approval. Then it asks "Is this ticket finished?" — a yes is the permission (no extra asks): commit any
51
52
  uncommitted session files, push each
52
53
  repo with work, open or update a PR per repo, post a PM comment, set status
53
54
  `development finished`. The PM comment is plain bullets for a non-technical PM: "What
@@ -72,18 +73,18 @@ writes, and agents until the session has a ticket. These replaced `/kickoff`, `/
72
73
  ### Status flow
73
74
 
74
75
  `to do` -> `in progress` (`/start`, only from `to do`, never backward) -> `development finished`
75
- (`/finish`). The `start-gate` guard allows only those two status values.
76
+ (`/finish`). Since 2026-10-15 the `start-gate` guard allows any status on the session ticket.
76
77
 
77
78
  ### Ticket-write rules
78
79
 
79
- - Writes go only to the session's own ticket — the `start-gate` hook blocks any other task id,
80
- and blocks task creation, deletion, moves, tags, links, docs, chat, and time tracking.
81
- - The `start-gate` hook is the hard limit: session ticket only; fields Application,
82
- Stakeholders, Task Type, Pseudocode, Has UI Changes, and the name; statuses `in progress` and
83
- `development finished` only.
84
- - No permission prompt for `clickup_update_task` or `clickup_create_comment` (since 2026-10-09).
85
- Field changes are written only after the developer's yes in chat; status changes and the PM
86
- comment need no question. Only `Bash(git push *)` stays in `permissions.ask`. `npx toga-ai`
80
+ - **No approval for ticket writes (team rule, 2026-10-15).** Claude updates the session
81
+ ticket's fields, name, status, description, tags, priority, assignees and comments itself,
82
+ then reports before → after.
83
+ - The `start-gate` hook keeps two hard limits: writes go only to the session's own ticket (any
84
+ other task id is blocked), and never to Has Code Changes (worker2 owns it). It also blocks task
85
+ creation, deletion, moves, links, docs, chat, time tracking and `update_comment` (no task id
86
+ to check).
87
+ - No permission prompt for `clickup_update_task` or `clickup_create_comment` (since 2026-10-09). Only `Bash(git push *)` stays in `permissions.ask`. `npx toga-ai`
87
88
  strips the old ClickUp rules from a dev's existing `permissions.ask` (see harness distribution).
88
89
  - Use only the `clickup` MCP server (`mcp__clickup__clickup_*`). The old read-only
89
90
  "claude.ai ClickUP for Dev" server (`mcp__claude_ai_ClickUP_for_Dev__*`) is blocked.
@@ -134,8 +135,9 @@ All tested 2026-10-08:
134
135
  .scss .less .html`, a React app's `src/`, blox components), else unchecks it.
135
136
  - **Branch = bare ticket id**, created by ClickUp or by `/start`'s `create-branch`. A branch
136
137
  named with the ticket id should link to the ticket through the GitHub integration — confirm on
137
- the first real `create-branch` run. PR base = the repo's default branch (usually
138
- `_production`, varies) — resolve with `gh repo view <owner/repo> --json defaultBranchRef`.
138
+ the first real `create-branch` run. Base branch is set by the ticket's project (TOGa Desk / TOGa IQ ticket → every repo from
139
+ `toga25`; else each repo's default branch) — rule lives in `rules/common/git-workflow.md`
140
+ → *Base branch*.
139
141
  - **The KB repo (`toga-tech`) works on `_main`.** No branch check and no PRs there; `/finish`
140
142
  still does the ticket check.
141
143
  - **Re-check the branch, never repeat it.** When the developer says they switched (or before
@@ -157,6 +159,10 @@ All tested 2026-10-08:
157
159
 
158
160
  ## Change history
159
161
 
162
+ - 2026-10-15 — Ticket writes need no approval: `/finish` writes fields itself, `/pseudocode`
163
+ writes once the plan is agreed; the gate allows any field/status/tag/comment on the session
164
+ ticket, except Has Code Changes. (jcardinal)
165
+ - 2026-10-15 — Base branch now follows the ticket's project (Desk/IQ → `toga25`); points at git-workflow.md. (jcardinal)
160
166
  - 2026-10-08 — Created: ClickUp-tied sessions (`/start`, `/commit`, `/pseudocode`, `/finish`)
161
167
  replace `/kickoff`, `/capture`, `/plan-ticket`, `/work-ticket`, `/rework-ticket`. (jcardinal)
162
168
  - 2026-10-08 — Branch claims always re-checked fresh; `/finish` runs `/commit` on yes and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1024",
3
+ "version": "1.0.1026",
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",
@@ -48,9 +48,28 @@ What you need to do:
48
48
  1. Run `2026-08-31a - AddColumn.sql` on the Core database (prod).
49
49
  2. Then run `2026-08-31b - Seed.sql` on the same database.
50
50
  3. Deploy worker2 to Elastic Beanstalk.
51
- 4. Confirm the WorkerJobs table shows the new column.
51
+ 4. Tell me when done — I check the new column myself.
52
52
  ```
53
53
 
54
+ ### Database changes: give a SQL file, not a description (team rule, 2026-10-15)
55
+
56
+ Applies to every repo. When a developer must change data or schema — ACL grant rows, seeds,
57
+ data cleanup, anything — Claude delivers a **ready-to-run SQL file**, never a prose list of
58
+ rows for the developer to write by hand.
59
+
60
+ 1. **Put the file in the right folder:** `dbchanges2` for 2.0, `dbchanges` for 1.0. Follow the
61
+ existing SQL file rules (file name with today's date, `ADD COLUMN ... AFTER`, random v4 uuids,
62
+ guarded re-runnable seeds, one database family per query — see the `dbchanges2`
63
+ architecture doc and `security.md`).
64
+ 2. **Add a "What you need to do" numbered run list** (format above): which file, which
65
+ database, in what order. For a `dbchanges2` `Client/` file the step always says
66
+ **"every client DB"**.
67
+ 3. **ACL grants stay the developer's call.** Claude proposes the roles in the file header
68
+ comment; the developer decides before running.
69
+ 4. **Put verify SELECTs at the end of the file.** After the developer runs it, Claude runs
70
+ those SELECTs itself (local DB, or `toga-db` read-only) and reports the result in plain
71
+ words. Never ask the developer to read verify output.
72
+
54
73
  ## Cut output tokens — the safe way
55
74
 
56
75
  Shorter answers are clearer AND cheaper to run. Both matter. But **clarity always wins** —
@@ -16,11 +16,14 @@ the commit prefix, the PR title, and the ticket updates at `/finish`.
16
16
  2. **`/finish`** does the KB capture, then asks one question: **"Is this ticket finished?"**
17
17
  On yes it: commits any uncommitted session files, pushes, opens (or updates) one PR per
18
18
  repo, comments on the ticket, and sets the status. No extra "may I push?" or "may I open
19
- the PR?" questions — that yes is the permission. Ticket field changes still use the one
20
- approval list.
21
- 3. **Branches.** Claude may create or switch to the ticket branch as part of normal work.
22
- 4. `/ship`, `/land-and-deploy` — the commits, pushes, and PRs they describe.
23
- 5. The KB repo (`~/toga-tech`) — `/finish` commits and pushes to `_main`.
19
+ the PR?" questions — that yes is the permission.
20
+ 3. **ClickUp ticket writes need no approval** (decided 2026-10-15, jcardinal). Claude updates
21
+ the session ticket's fields, title, status and comments itself, then reports before → after.
22
+ Only the session's own ticket, and never Has Code Changes (worker2 owns it) — the
23
+ `start-gate` hook enforces both.
24
+ 4. **Branches.** Claude may create or switch to the ticket branch as part of normal work.
25
+ 5. `/ship`, `/land-and-deploy` — the commits, pushes, and PRs they describe.
26
+ 6. The KB repo (`~/toga-tech`) — `/finish` commits and pushes to `_main`.
24
27
 
25
28
  Always, in every repo:
26
29
 
@@ -47,7 +50,7 @@ either (see *Claude never rewrites code because a branch changed*). Present diff
47
50
  let the developer commit, PR, and deploy.
48
51
 
49
52
  **One exception — `/start`'s `create-branch`.** At `/start`, after the developer says yes for
50
- that repo, Claude may run `start-helper.js create-branch` to create (from the default branch)
53
+ that repo, Claude may run `start-helper.js create-branch` to create (from the ticket's base branch, see *Branch naming*)
51
54
  or check out the ticket branch `<TICKET>`. Nothing else: no worktree, no stash, never on a
52
55
  repo with uncommitted changes, never force-push. See *Branch naming*.
53
56
 
@@ -99,14 +102,23 @@ Never force-push to `_main` under any circumstances.
99
102
  The branch is the bare ClickUp ticket id — e.g. `TRUE-82600`. No prefix, no description.
100
103
  ClickUp may create it, or `/start` creates it. One branch per ticket per repo.
101
104
 
102
- **Base branch.** Only the new platform app repos (today `toga25-desk` and `toga25-iq`) branch from
103
- `toga25` and open their PR back to `toga25`. Every other repo (`_underscore`, `api2`, `worker2`,
104
- `dbchanges2`, …) uses its own default branch, even in the same session.
105
+ **Base branch.** The ticket's project (its Application) decides the base — not the repo.
106
+
107
+ - **New platform app ticket** (today TOGa Desk = `toga25-desk`, TOGa IQ = `toga25-iq`): every repo
108
+ touched for that ticket — the app and its dependencies (`toga25-blox`, `toga2-template`,
109
+ `_underscore`, `api2`, `dbchanges2`, `worker2`, …) — branches from `toga25`. Its PR goes back to
110
+ `toga25`. At `/start`, pass `--base toga25` to `create-branch`.
111
+ - **Any other project** (e.g. TOGa Commerce, TOGa Supply, Worker): every repo branches from its own
112
+ default branch (e.g. `_production`). PRs go there — even when the ticket touches the same shared
113
+ repos (`_underscore`, `api2`, …).
114
+ - Example: a `toga25-desk` ticket touching `_underscore` → `_underscore` branches from `toga25`. A
115
+ `toga2-commerce` ticket touching `_underscore` → from `_production`.
116
+ - The KB repo `toga-tech` is unchanged: `_main`, no branch, no PR.
105
117
 
106
118
  `/start` checks that every repo on the ticket (from its Application values) is on the ticket
107
119
  branch. On a mismatch it asks once per repo: "Create/switch to `<TICKET>` in `<repo>` from
108
120
  `<base>`?" On yes it runs `start-helper.js create-branch` (checks out `origin/<TICKET>` or the
109
- local branch if one exists, else creates it from the default branch and pushes it with
121
+ local branch if one exists, else creates it from the base branch above and pushes it with
110
122
  `git push -u`, never force). A repo with uncommitted changes is refused — Claude stops and
111
123
  says so. Claude may also create or switch to the ticket branch later in normal work, with the
112
124
  same limits.
@@ -75,6 +75,9 @@ escaping, and it will corrupt data you meant to store intact.
75
75
 
76
76
  ## Never query across database clusters (2.0)
77
77
 
78
+ (Handing a DB change to a developer? Give a SQL file — see `communication.md` § *Database
79
+ changes: give a SQL file, not a description*.)
80
+
78
81
  In 2.0 the database families live on **separate production clusters**: `Core` / `Forecast` /
79
82
  `Team` (prod-core), `Client_<Tenant>` (prod-client), `Archive_<Tenant>` (prod-archive), and
80
83
  `Logs` / `Logs_<Tenant>` (prod-logs). A single SQL statement that joins or sub-selects across
@@ -33,11 +33,18 @@
33
33
  * the team repo.
34
34
  *
35
35
  * ClickUp guard (any stage, any tool name containing "clickup"): an ALLOWLIST. Reads pass.
36
- * Writes pass only for `mcp__clickup__clickup_update_task` (allowed keys and fields only; status
37
- * only "in progress" or "development finished") and
38
- * `mcp__clickup__clickup_create_comment` (entity_type "task"), on the session ticket. Everything
39
- * else is blocked, including every tool of the old "ClickUP for Dev" server. This branch FAILS
40
- * CLOSED on an internal error; the rest of the gate fails open.
36
+ * Team rule 2026-10-15 (jcardinal): Claude may update the SESSION ticket's fields, name, status,
37
+ * description, tags, priority, assignees and comments without asking. So on the session ticket
38
+ * these writes pass: `update_task` (keys in UPDATE_TASK_KEYS, any status, any custom field),
39
+ * `add_tag_to_task` / `remove_tag_from_task`, and `create_comment` (entity_type "task") /
40
+ * `create_task_comment`. Two guards stay:
41
+ * 1. Session ticket only — never another task. A wrong id would silently change someone
42
+ * else's ticket, and the task id is the only thing tying a write to this session.
43
+ * 2. Never Has Code Changes — worker2 sets it from the PRs; a Claude write would fight it.
44
+ * Everything else is blocked (create/delete/move tasks, links, docs, chat, time tracking,
45
+ * `update_comment` — it carries no task id, so the ticket cannot be checked), including every
46
+ * tool of the old "ClickUP for Dev" server. This branch FAILS CLOSED on an internal error; the
47
+ * rest of the gate fails open.
41
48
  *
42
49
  * Subagents: hook input carries `agent_id` (only inside a subagent) and `agent_type`. The
43
50
  * ticket-reader subagent may run Bash only as `node <pinned>/ticket-parse.js ...`.
@@ -101,17 +108,15 @@ const MAX_UNWRAP_DEPTH = 6;
101
108
  const OLD_CLICKUP_SERVER_RE = /^claude_ai_ClickUP_for_Dev$/i;
102
109
  /* get_* covers get_operators and get_schema. download_* is a read but is not needed — blocked. */
103
110
  const CLICKUP_READ_RE = /^(get_.+|search|search_.+|filter_.+|list_.+|find_.+|resolve_assignees)$/;
104
- const UPDATE_TASK_KEYS = new Set(['task_id', 'workspace_id', 'status', 'name', 'custom_fields']);
105
- const CUSTOM_FIELD_ITEM_KEYS = new Set(['id', 'value']);
106
- /* /start sets "in progress" (only from "to do"); /finish sets "development finished". */
107
- const ALLOWED_STATUSES = new Set(['in progress', 'development finished']);
108
- const WRITABLE_FIELDS = new Map([
109
- ['32e9fcd3-6f59-448a-a06e-de52afc87587', 'Application'],
110
- ['ec15edfd-5920-4c12-a1dc-94ede4a62438', 'Stakeholders'],
111
- ['093170c3-abdc-4815-9f1f-f51841799590', 'Task Type'],
112
- ['ad0e4fc8-ff03-4e5f-a187-7d2cd6d0bc65', 'Pseudocode'],
113
- ['576ff7b7-a2bf-4e78-bd55-85661d1a98ef', 'Has UI Changes'],
111
+ /* Any field of the session ticket (team rule 2026-10-15). Not `task_type`: Task Type is a custom field here. */
112
+ const UPDATE_TASK_KEYS = new Set([
113
+ 'task_id', 'workspace_id', 'status', 'name', 'custom_fields', 'markdown_description', 'priority',
114
+ 'assignees', 'due_date', 'start_date', 'time_estimate',
114
115
  ]);
116
+ const CUSTOM_FIELD_ITEM_KEYS = new Set(['id', 'value']);
117
+ /* Writes on the session ticket keyed by task_id. */
118
+ const TASK_ID_WRITE_OPS = new Set(['update_task', 'add_tag_to_task', 'remove_tag_from_task', 'create_task_comment']);
119
+ /* worker2 owns it (sets it from the ticket's PRs) — Claude never writes it. */
115
120
  const HAS_CODE_CHANGES_FIELD = 'af9eaba8-d3d3-4769-b8bc-28de78b04570';
116
121
 
117
122
  /* ---------------- shell allowlist data ---------------- */
@@ -186,10 +191,10 @@ const MSG = {
186
191
  noSessionTicket: 'ClickUp write blocked: this session has no ticket. Run /start TRUE-1234 first.',
187
192
  alwaysBlocked: (op) => 'ClickUp call blocked: `' + op + '` is not allowed from Claude. Ask the developer to do it in ClickUp.',
188
193
  commentEntity: 'ClickUp write blocked: comments may only go on the session ticket (entity_type "task").',
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).',
190
- badStatus: 'ClickUp write blocked: the only statuses Claude may set are "in progress" (/start) and "development finished" (/finish).',
194
+ badKey: (k) => 'ClickUp write blocked: update_task may not set `' + k + '`. Allowed: ' + [...UPDATE_TASK_KEYS].filter((x) => x !== 'task_id' && x !== 'workspace_id').join(', ') + '.',
195
+ badStatus: 'ClickUp write blocked: status must be a non-empty string.',
191
196
  hasCodeChanges: 'ClickUp write blocked: never write Has Code Changes — worker2 owns it.',
192
- badField: (id) => 'ClickUp write blocked: custom field `' + id + '` is not one Claude may write.',
197
+ badField: (id) => 'ClickUp write blocked: custom field item `' + id + '` must be {id, value}.',
193
198
  guardError: 'ClickUp call blocked: the start gate could not check it (internal error). Try again; if it repeats, tell the developer.',
194
199
  };
195
200
 
@@ -762,8 +767,8 @@ function checkCustomFields(value) {
762
767
  for (const item of list) {
763
768
  if (!item || typeof item !== 'object' || Array.isArray(item)) return MSG.badField(JSON.stringify(item));
764
769
  const id = String(item.id || '');
765
- if (id === HAS_CODE_CHANGES_FIELD) return MSG.hasCodeChanges;
766
- if (!WRITABLE_FIELDS.has(id)) return MSG.badField(id || 'no id');
770
+ if (id.trim().toLowerCase() === HAS_CODE_CHANGES_FIELD) return MSG.hasCodeChanges;
771
+ if (!id) return MSG.badField('no id');
767
772
  const extra = Object.keys(item).find((k) => !CUSTOM_FIELD_ITEM_KEYS.has(k));
768
773
  if (extra) return MSG.badKey('custom_fields[].' + extra);
769
774
  }
@@ -772,7 +777,7 @@ function checkCustomFields(value) {
772
777
 
773
778
  function checkUpdateTask(ti) {
774
779
  for (const k of Object.keys(ti)) if (!UPDATE_TASK_KEYS.has(k)) return MSG.badKey(k);
775
- if ('status' in ti && (typeof ti.status !== 'string' || !ALLOWED_STATUSES.has(ti.status.trim().toLowerCase()))) return MSG.badStatus;
780
+ if ('status' in ti && (typeof ti.status !== 'string' || !ti.status.trim())) return MSG.badStatus;
776
781
  if ('name' in ti && typeof ti.name !== 'string') return MSG.badKey('name');
777
782
  if ('custom_fields' in ti) return checkCustomFields(ti.custom_fields);
778
783
  return null;
@@ -786,7 +791,7 @@ function clickupVerdict(tool, toolInput, sid) {
786
791
  const op = m[2].replace(/^clickup_/i, '').toLowerCase();
787
792
  if (OLD_CLICKUP_SERVER_RE.test(server)) return MSG.oldClickup;
788
793
  if (CLICKUP_READ_RE.test(op)) return null;
789
- if (op !== 'update_task' && op !== 'create_comment') return MSG.alwaysBlocked(m[2]);
794
+ if (op !== 'create_comment' && !TASK_ID_WRITE_OPS.has(op)) return MSG.alwaysBlocked(m[2]);
790
795
  if (server !== CLICKUP_SERVER) return MSG.otherServer(server);
791
796
  const ti = toolInput && typeof toolInput === 'object' && !Array.isArray(toolInput) ? toolInput : {};
792
797
  const session = sid ? readSession(sid) : null;
@@ -796,7 +801,7 @@ function clickupVerdict(tool, toolInput, sid) {
796
801
  return sameTicket(ti.entity_id || '', session) ? null : MSG.otherTicket(String(ti.entity_id || 'no task id'));
797
802
  }
798
803
  if (!sameTicket(ti.task_id || '', session)) return MSG.otherTicket(String(ti.task_id || 'no task id'));
799
- return checkUpdateTask(ti);
804
+ return op === 'update_task' ? checkUpdateTask(ti) : null;
800
805
  }
801
806
 
802
807
  function clickupGuard(tool, toolInput, sid) {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: finish
3
- description: End-of-session closer for TOGA Technology work tied to a ClickUp ticket (replaces /capture). Always records the session's durable knowledge into the team `claude` knowledge base first (search, classify CREATE/UPDATE/DEPRECATE/NO-OP, write, publish to `_main`). Then checks the session ticket's fields (Application, Stakeholders, Task Type, title, Has UI Changes) in ONE approval list, asks "Is this ticket finished?", and if yes pushes the ticket branch in every repo with work, opens or updates one PR per repo (with the full review summary in the description), posts a short PM comment, and sets status `development finished`. Run at the END of EVERY session, finished or not. Trigger on "/finish", "finish", "wrap up", "capture", "save my work to the knowledge base", "I'm done", or when the developer ends a session.
3
+ description: End-of-session closer for TOGA Technology work tied to a ClickUp ticket (replaces /capture). Always records the session's durable knowledge into the team `claude` knowledge base first (search, classify CREATE/UPDATE/DEPRECATE/NO-OP, write, publish to `_main`). Then checks the session ticket's fields (Application, Stakeholders, Task Type, title, Has UI Changes) and updates them itself in one call (no approval needed), asks "Is this ticket finished?", and if yes pushes the ticket branch in every repo with work, opens or updates one PR per repo (with the full review summary in the description), posts a short PM comment, and sets status `development finished`. Run at the END of EVERY session, finished or not. Trigger on "/finish", "finish", "wrap up", "capture", "save my work to the knowledge base", "I'm done", or when the developer ends a session.
4
4
  ---
5
5
 
6
6
  # /finish — save the knowledge, check the ticket, ship the ticket
@@ -12,8 +12,9 @@ Keep the main thread lean: the KB write pipeline runs in the `session-capture` s
12
12
  ClickUp reads run in the `ticket-reader` subagent. The main thread distills and asks only for real decisions.
13
13
 
14
14
  Claude writes and posts all summary text itself — PM comment, PR subject and body, commit
15
- notes, KB capture summary. Never ask the developer to approve that wording. Approvals are only
16
- for real decisions: ticket field changes (one list), ELEVATED KB docs, and "Is this ticket finished?". A yes to
15
+ notes, KB capture summary. Never ask the developer to approve that wording. Claude also
16
+ updates the session ticket's fields, status and comments itself — no approval (team rule,
17
+ 2026-10-15). Approvals are only for real decisions: ELEVATED KB docs and "Is this ticket finished?". A yes to
17
18
  that question is the permission to commit, push, and open the PRs — no extra asks.
18
19
 
19
20
  ## Step 0 — Rules, helpers, session ticket
@@ -27,18 +28,18 @@ exactly these, nothing else):
27
28
  origin _main` (never force, no PR) — the KB repo exception in `rules/common/git-workflow.md`.
28
29
  4. `gh pr create` for those repos, or `gh pr edit <n> --body-file <file>` to rebuild the body of an open PR.
29
30
  5. ClickUp writes **on the session ticket only**, with exactly two tools:
30
- - `mcp__clickup__clickup_update_task` — only these: `name`, `status` = `development
31
- finished`, and the custom fields Application, Stakeholders, Task Type, Pseudocode,
32
- Has UI Changes.
31
+ - `mcp__clickup__clickup_update_task` — any field, name, status, description, tags,
32
+ priority or assignees of the session ticket. /finish uses `name`, `status` = `development
33
+ finished`, and the custom fields Application, Stakeholders, Task Type, Has UI Changes.
33
34
  - `mcp__clickup__clickup_create_comment` — one PM comment, `entity_type: "task"`.
34
35
 
35
36
  Never: force-push, push any other branch, commit source code (that is `/commit`), write the
36
37
  **Has Code Changes** field (`af9eaba8-d3d3-4769-b8bc-28de78b04570` — worker2 owns it), write
37
38
  "Pseudocode Review" / "Pseudocode Required", touch any other ticket, or use the old read-only
38
39
  `mcp__claude_ai_ClickUP_for_Dev__*` tools. The `start-gate` hook blocks every other ClickUp
39
- write (other tools, other fields, other tickets). Claude Code asks before `clickup_update_task`
40
- (field changes) — that prompt is expected. The PM comment posts without a prompt. `git push` may also ask for
41
- permission — that is expected too.
40
+ write (other tools, other tickets, Has Code Changes). Field changes, status and the PM comment
41
+ need no approval — write them, then report them. `git push` may ask for permission — that is
42
+ expected.
42
43
 
43
44
  **Helpers** (both print JSON):
44
45
 
@@ -241,7 +242,7 @@ validate → index → mirror → rebase-push command. The `PostToolUse` hook st
241
242
 
242
243
  ---
243
244
 
244
- # Part 2 — Ticket field check (ONE list, ONE approval)
245
+ # Part 2 — Ticket field check (Claude decides and writes — no approval)
245
246
 
246
247
  Skip Part 2 and Part 3 if the session has no verified ticket (Step 0).
247
248
 
@@ -274,34 +275,34 @@ Compare the ticket (Step 0 read) with what the session actually did:
274
275
  6. Never touch Has Code Changes.
275
276
 
276
277
  **Labels REPLACE the whole set.** For Application and Stakeholders, the new value is
277
- `current set ∪ additions − removals` — never just the additions. Only remove a value the
278
- developer agrees is wrong.
278
+ `current set ∪ additions − removals` — never just the additions. Remove a value only when the
279
+ work clearly shows it is wrong, and give the reason in the report.
279
280
 
280
- ## Step 9 — Show ONE numbered approval list
281
+ ## Step 9 — Decide the changes (no approval)
281
282
 
282
- Show only fields that change. Example:
283
+ Claude decides the changes itself. Do not ask the developer to approve them (team rule,
284
+ 2026-10-15). Keep a list of only the fields that change, each with the reason. Example:
283
285
 
284
286
  ```
285
- Ticket TRUE-82600 — proposed changes:
286
- 1. Application: add "TOGa Desk" (now: "api2") → api2, TOGa Desk
287
- 2. Stakeholders: add "Compass Group" (now: none)
288
- 3. Task Type: "Bug" → "Enhancement"
287
+ Ticket TRUE-82600 — changes:
288
+ 1. Application: add "TOGa Desk" (was: api2) → api2, TOGa Desk — work also changed the Desk app
289
+ 2. Stakeholders: add "Compass Group" (was: none) — fix is for Compass
290
+ 3. Task Type: "Bug" → "Enhancement" — new behavior, not a fix
289
291
  4. Title: "Fix thing" → "Compass - Fix order-for dropdown on Desk [Claude modified]"
290
- 5. Has UI Changes: true → false (no UI files changed)
291
- Approve all, some (give numbers), or none?
292
+ 5. Has UI Changes: true → false — no UI files changed
292
293
  ```
293
294
 
294
295
  No changes → say "Ticket fields look right — no changes." and go to Part 3.
295
296
 
296
- ## Step 10 — Write the approved changes (one call), read back, show the difference
297
+ ## Step 10 — Write the changes (one call), read back, report before → after
297
298
 
298
- One `mcp__clickup__clickup_update_task` call with only the approved items. Do **not** use the
299
+ One `mcp__clickup__clickup_update_task` call with all the changes from Step 9. Do **not** use the
299
300
  tool's `task_type` parameter — Task Type here is a custom field.
300
301
 
301
302
  ```
302
303
  mcp__clickup__clickup_update_task
303
304
  task_id: "<TICKET>" (custom id, e.g. "TRUE-82600")
304
- name: "<new title>" (only if item approved)
305
+ name: "<new title>" (only if the title changes)
305
306
  custom_fields: [
306
307
  {"id": "32e9fcd3-6f59-448a-a06e-de52afc87587", "value": "[\"<app uuid>\",\"<app uuid>\"]"},
307
308
  {"id": "ec15edfd-5920-4c12-a1dc-94ede4a62438", "value": "[\"<client uuid>\"]"},
@@ -313,9 +314,9 @@ mcp__clickup__clickup_update_task
313
314
  Value formats (all strings): labels = JSON array string of option UUIDs (the FULL set);
314
315
  drop_down = one option UUID; checkbox = `"true"` / `"false"`.
315
316
 
316
- Then spawn `ticket-reader` (`mode: ticket`) again and show the before → after for each changed
317
- field. If a value did not stick, say so plainly. Keep the list of changed fields for the PM
318
- comment.
317
+ Then spawn `ticket-reader` (`mode: ticket`) again and check each value stuck. If a value did
318
+ not stick, say so plainly. Keep the before → after list with reasons: it goes in the Step 17
319
+ report (and the "is this finished?" no-path report) and in the PM comment.
319
320
 
320
321
  ---
321
322
 
@@ -557,7 +558,7 @@ developer the list name and leave the status alone.
557
558
  Short list:
558
559
 
559
560
  1. KB: publish status (Step 5).
560
- 2. Ticket fields changed (or "none").
561
+ 2. Ticket fields changed: before → after with the reason, one line each (or "none").
561
562
  3. PRs: one url per repo, and which were created vs. updated.
562
563
  4. PM comment: posted (yes/no), with the posted text.
563
564
  5. Status: `development finished` (or why not).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pseudocode
3
- description: Write the agreed approach for a MAJOR ClickUp ticket into the ticket's Pseudocode field, as clean plain text, after the developer approves it. Writes the session ticket only, and only that one field. Trigger on "/pseudocode", "write the pseudocode", "put the plan on the ticket".
3
+ description: Write the agreed approach for a MAJOR ClickUp ticket into the ticket's Pseudocode field, as clean plain text, once the plan is agreed with the developer (no separate approval for the write). Writes the session ticket only, and only that one field. Trigger on "/pseudocode", "write the pseudocode", "put the plan on the ticket".
4
4
  ---
5
5
 
6
6
  # /pseudocode — put the agreed approach on the ticket
@@ -15,9 +15,9 @@ insists.
15
15
  (`ad0e4fc8-ff03-4e5f-a187-7d2cd6d0bc65`). Never "Pseudocode Review", "Pseudocode Required",
16
16
  or any other field.
17
17
  - The only write is `mcp__clickup__clickup_update_task` with that one custom field. Never
18
- `mcp__claude_ai_ClickUP_for_Dev__*`. The `start-gate` hook blocks any other ClickUp write,
19
- field, or ticket.
20
- - Claude Code asks the developer before the write. That prompt is expected.
18
+ `mcp__claude_ai_ClickUP_for_Dev__*`. The `start-gate` hook blocks writes to any other ticket.
19
+ - The plan itself must be agreed with the developer. The write needs no extra approval (team
20
+ rule, 2026-10-15) — once the plan is agreed, write it and report.
21
21
 
22
22
  ## Step 1 — Session ticket
23
23
 
@@ -67,19 +67,20 @@ OUT OF SCOPE
67
67
  1. Email notices.
68
68
  ```
69
69
 
70
- ## Step 4 — Show it and get approval
70
+ ## Step 4 — Check the plan is agreed
71
71
 
72
- Show the text in a code block. If the field already has text, show **old** and **new** one
73
- after the other and ask: "Replace the current Pseudocode with this?" Edit until the developer
74
- approves. No approval → do not write.
72
+ The text must match the approach agreed with the developer in this session. If no approach was
73
+ agreed yet, agree it first (that is a real decision — ask). Once agreed, do not ask again
74
+ before writing. If the field already has text, keep what is still true and replace the rest;
75
+ show **old** and **new** in the Step 5 report.
75
76
 
76
77
  ## Step 5 — Write, then read back
77
78
 
78
79
  ```
79
80
  mcp__clickup__clickup_update_task
80
81
  task_id: "<TICKET>"
81
- custom_fields: [{"id": "ad0e4fc8-ff03-4e5f-a187-7d2cd6d0bc65", "value": "<approved plain text>"}]
82
+ custom_fields: [{"id": "ad0e4fc8-ff03-4e5f-a187-7d2cd6d0bc65", "value": "<agreed plain text>"}]
82
83
  ```
83
84
 
84
85
  Spawn `ticket-reader` (`mode: ticket`) again and confirm its `pseudocode` matches. Report: "Pseudocode
85
- written to <TICKET>." or what did not stick.
86
+ written to <TICKET>." (with old → new if it replaced text) or what did not stick.
@@ -24,11 +24,11 @@ The hook already checked the id, wrote the session ticket file
24
24
  main session. Repo code reads, edits and every agent stay blocked until you run
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
- `mcp__clickup__clickup_update_task` (fields: Application, Stakeholders, Task Type,
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).
27
+ `mcp__clickup__clickup_update_task` (any field, name, status, description, tags, priority,
28
+ assignees — never Has Code Changes) and `mcp__clickup__clickup_create_comment`. The hook
29
+ blocks other tickets and every other ClickUp write. Field, status and comment writes need
30
+ no approval (team rule, 2026-10-15): write them, then tell the developer before → after.
31
+ /start writes only the status `to do` -> `in progress` (Step 6b).
32
32
 
33
33
  Trailing text is never permission to skip steps. If a block message appears, do the step it
34
34
  names. Do not fight the gate. (Harness work only: `TOGA_START_GATE_DISABLED=1`.)
@@ -140,10 +140,13 @@ developer has not named the session.
140
140
  — ask for the local path.
141
141
 
142
142
  3. **For each repo in `mismatches`, ask once:** "Create/switch to `<T>` in `<repo>` from
143
- `<base>`?" Resolve `<base>` with
143
+ `<base>`?" The base comes from the
144
+ TICKET's project, not the repo. New platform app ticket (TOGa Desk / TOGa IQ): `<base>` =
145
+ `toga25` for EVERY repo on the ticket (app + dependencies). Any other project: the repo's own
146
+ default branch — resolve with
144
147
  `gh repo view <owner/repo> --json defaultBranchRef -q .defaultBranchRef.name` when gh works
145
148
  (owner/repo from `git -C <repo> remote get-url origin`); else leave `--base` off (origin's
146
- default is used). If `changes` > 0, do not offer — STOP and say: "`<repo>` has uncommitted
149
+ default is used). See `rules/common/git-workflow.md` § Branch naming. If `changes` > 0, do not offer — STOP and say: "`<repo>` has uncommitted
147
150
  changes on `<branch>`. Commit or move them, then tell me." Never stash.
148
151
 
149
152
  4. **On yes:**
@@ -264,12 +267,14 @@ KB loaded: <doc titles>; gaps: <none | list>
264
267
 
265
268
  State the permission policy in one line: "File reads and local edits run automatically;
266
269
  anything that executes, deploys, hits a remote/production environment, or leaves this machine,
267
- I'll confirm with you first. Ticket writes always ask you first."
270
+ I'll confirm with you first. I update this ticket's fields, status and comments myself and
271
+ tell you what changed."
268
272
 
269
273
  ## During the session
270
274
 
271
275
  - If you notice another app or client is affected, or the title is far off, note it for
272
- `/finish`. Do not write to the ticket now.
276
+ `/finish` (it writes all field changes in one call). You may also update the ticket now
277
+ without asking — then tell the developer before → after in one line.
273
278
  - Commit with `/commit`. `/finish` pushes, opens PRs, and updates the ticket.
274
279
  - `/pseudocode` writes the agreed plan to the ticket (major tickets only).
275
280