toga-ai 1.0.991 → 1.0.992

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.
@@ -8,7 +8,7 @@
8
8
  | [Developer Dashboard (ClickUp Sprint, tools /developer)](features/clickup-sprint-dashboard.md) | Native ClickUp sprint dashboard in the 1.0 tools app at `/developer` (TV wall display), reading the Team DB via `/v2/sprints/*`; open for the data engine, the C |
9
9
  | [CloudFront Client Setup](features/cloudfront-client-setup.md) | SSO-gated Tools admin at `/devops/cloudfront-clients` that onboards a client onto CloudFront + Route 53 across multiple AWS accounts in one run; open for the to |
10
10
  | [Compass User & Persona Admin (tools /compass/users, /compass/personas)](features/compass-user-persona-admin.md) | Two pages in the Tools `developers` nav folder that let staff do, in a browser, the Compass requests that used to mean "email a developer, who writes a `dbchang |
11
- | [Design Demo Admin](features/design-demo-admin.md) | SSO-gated Tools admin at `/design` that publishes self-contained "Claude Design" HTML exports as versioned demos into the `forward` repo over the GitHub REST AP |
11
+ | [Design Demo Admin](features/design-demo-admin.md) | SSO-gated Tools admin at `/design` that publishes self-contained "Claude Design" HTML exports as versioned demos into S3 bucket `togatech-demos` (served by Clou |
12
12
  | [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | Tools "Developers" folder — the Generate UUID and Generate Password tools (reference example of the one-tool-per-`mvc/<route>/get.php` pattern); open for the ge |
13
13
  | [/emails Email Queue Review (Tools → 2.0 Logs_<Client>.Email)](features/email-queue-review.md) | Staff page to browse any 2.0 client's `Logs_<Client>.Email` queue and release/reject `HELD` review-hold emails. |
14
14
  | [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only 1.0 Tools triage/curation screen (`/errors`) for the 2.0 Issue/Event error pipeline, reading the shared Core Logs DB via `db_toga2logs`; curates, |
@@ -6,10 +6,10 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-08
9
+ updated: 2026-10-09
10
10
  owners: [jcardinal]
11
11
  files:
12
- - tools/_/app/design/github.php
12
+ - tools/_/app/design/s3.php
13
13
  - tools/mvc/design/get.php
14
14
  - tools/mvc/design/post.php
15
15
  - tools/assets/css/design.css
@@ -27,88 +27,96 @@ related:
27
27
  - ../../../standalone/apps/forward/features/static-demo-hosting.md
28
28
  ---
29
29
 
30
- SSO-gated Tools admin at `/design` that publishes self-contained "Claude Design" HTML exports as versioned demos into the `forward` repo over the GitHub REST API; open for the GitHub Git-Data commit model, the plain-text/escape invariant, or the curl_close 8.5 gotcha.
30
+ SSO-gated Tools admin at `/design` that publishes self-contained "Claude Design" HTML exports as versioned demos into S3 bucket `togatech-demos` (served by CloudFront at `demo.togatech.com`); open for the S3 conditional-write model, publish order, credentials, the plain-text/escape invariant, or the AWS setup.
31
31
 
32
32
  ## Summary
33
33
 
34
- A self-serve admin UI at **`/design`** in the SSO-protected **Tools** app that lets the design team publish self-contained "Claude Design" HTML exports as **versioned demos** — no developer, no git, no local checkout. It writes into the **`agilantsolutions/forward`** repo (branch `_main`) over the **GitHub REST API**, and the demos stay **publicly hosted by `forward`** at `demo.togatech.com` (URLs unchanged). See [Static Demo Hosting](../../../standalone/apps/forward/features/static-demo-hosting.md).
34
+ Self-serve admin at **`/design`** in the SSO-protected **Tools** app. The design team publishes Claude Design HTML exports as **versioned demos** — no developer, no git. Storage = S3 bucket **`togatech-demos`**; public hosting = CloudFront at **`demo.togatech.com`** (URLs unchanged). Hosting setup, cache rules, and rollback: [Static Demo Hosting](../../../standalone/apps/forward/features/static-demo-hosting.md).
35
35
 
36
- The tool **moved out of `forward` into `tools` this session.** Rationale: `forward` has no auth (it is only a forwarder/hoster); `tools` is SSO-protected and is the home for internal tools. The admin UI is now behind SSO, but the data (demos + manifests) still lives in and is served from the `forward` repo.
36
+ Since 2026-10-09 (TRUE-82616) storage is S3, not the `forward` GitHub repo. `App_Design_Github` was deleted and replaced by **`App_Design_S3`** with the same `post.php` actions.
37
37
 
38
38
  ## How it works
39
39
 
40
40
  ### Key files / entry points
41
- - `_/app/design/github.php` — class **`App_Design_Github`**: all GitHub REST + domain logic ported from the old single-file tool (`ghApi` / `ghListDir` / `ghReadFile` / `ghCommitFiles` / `ghDeletePaths`, project/version/tab CRUD, upload validation, redirect-stub builder).
41
+ - `_/app/design/s3.php` — class **`App_Design_S3`**: AWS S3/CloudFront clients, conditional writes, project/version/tab CRUD, upload checks, stub builder, Claude prompt. Constants: `BUCKET`, `REGION` (us-west-2), `DISTRIBUTION_ID`, `MAX_UPLOAD_BYTES`, `MAX_CONFLICT_RETRIES`, cache-control values.
42
42
  - `mvc/design/get.php` — route `/design` (page shell). Self-guards with `App_Auth::requireAuth([...])` + `App_Page::validateCrossSiteRequestForgery()`.
43
43
  - `mvc/design/post.php` — AJAX action dispatcher.
44
- - `assets/css/design.css`, `assets/js/design.js` — namespaced under `.design-tool`, registered globally in `_/app/frameworkindex.php`.
45
- - `_/app/nav.php` — a `design` group, action **`Design Demos`** → `/design`, personas `['TOGa Technology','Development Team']`.
46
-
47
- ### GitHub API access (no filesystem)
48
- The tool runs on the SSO-protected Tools EB host, not a git checkout, so **all** repo access is over the GitHub API:
49
- - **Reads / listing:** GitHub **Contents API** (`ghReadFile` uses the raw media type `Accept: application/vnd.github.raw` so multi-MB exports stream up to 100 MB — the base64 Contents response inlines only ≤1 MB and silently returns empty content above that).
50
- - **Writes:** GitHub **Git Data API** — blob → tree → commit → update-ref — so a publish is an **atomic multi-file commit** (version files + regenerated stub + `project.json` land in one commit).
51
- - Publishing pushes to `forward`'s `_main`, which triggers `forward`'s EB auto-deploy — demos go live in a few minutes; there is no staging step.
52
-
53
- ### Data model (lives in the `forward` repo)
54
- - Per project: **`<project>/project.json`** — `{ title, latest, hidden, tabs[], versions[] }`. Human detail (label/author/notes/date) lives in the manifest, not the URL. A directory is a managed demo **iff it contains `project.json`**.
55
- - Per version: **`<project>/vN/index.html`** — sequential `/v1`, `/v2`, `/v3` URL folders (not semver, not date-based).
56
- - Repo root **`tabs.json`** — `{ "tabs": [...] }`, the ordered tab list.
57
- - The bare `/<project>` serves an auto-generated `index.html` **stub** that meta-refresh/JS-redirects (relative `./vN/`) to the newest **visible** version; regenerated on every publish/unlist.
44
+ - `assets/css/design.css`, `assets/js/design.js` — namespaced under `.design-tool`, registered in `_/app/frameworkindex.php`.
45
+ - `_/app/nav.php` — `design` group, action **`Design Demos`** → `/design`, personas `['TOGa Technology','Development Team']`.
46
+
47
+ ### Data model (S3 keys = URL paths)
48
+ - `<project>/project.json` — `{ title, latest, hidden, tabs[], versions[] }`. A folder is a managed demo **iff it has `project.json`**.
49
+ - `<project>/vN/index.html` — sequential `/v1`, `/v2`… (not semver/date).
50
+ - Root `tabs.json` — `{ "tabs": [...] }`, ordered tab list.
51
+ - `<project>/index.html` — generated **stub** that redirects (relative `./vN/`) to the newest **visible** version; rewritten on publish/unlist.
52
+ - Root `404.html` — CloudFront error page.
53
+ - `design/` and `sos/` are excluded from listing (`EXCLUDED_FOLDERS`).
54
+
55
+ ### Conditional writes (no lost updates)
56
+ S3 has no multi-file commit, so writes use S3 conditional PUT (vendored aws-sdk-php 3.388.13 supports both):
57
+ - Create-only files: `PutObject IfNoneMatch '*'`.
58
+ - Updates of `project.json` / `tabs.json`: `PutObject IfMatch <ETag read earlier>`.
59
+ - On 412/409: re-read and retry, up to `MAX_CONFLICT_RETRIES` (3). Then the user sees *"Another change was just published — please try again."*
60
+ - **Publish order:** version file (create-only) → `project.json` → stub. Next version id = max of `project.json` versions and existing `vN/` prefixes, so a half-finished publish never reuses an id.
61
+ - **Tab rename/delete:** rewrite each affected `project.json` first, `tabs.json` last. Not one atomic step any more, but safe to re-run.
62
+ - **Cache headers:** `vN/index.html` `max-age=86400`; stub, `project.json`, `tabs.json` `max-age=60`. A normal publish needs no CloudFront invalidation (live within ~1 minute).
58
63
 
59
64
  ### Multi-tab membership
60
- The project.json `tab` **scalar** became a **`tabs` array**. It is read **backward-compatibly** (an old `tab` scalar is honored) and **lazily migrated** — the scalar is dropped on the next write of that project.json. Tab management via a per-pill "⋯" menu:
61
- - **Rename / delete** — rewrite `tabs.json` **and** every affected `project.json` in one **atomic** commit.
62
- - **Reorder** — a single-file `tabs.json` commit, validated as a **permutation** of the existing list.
65
+ `project.json` `tab` scalar → `tabs` array. Old scalar is read (backward-compatible) and dropped on the next write. Per-pill "⋯" menu: rename / delete (see above), reorder (one `tabs.json` write, validated as a permutation of the existing list).
63
66
 
64
67
  ### Text-storage invariant: JSON holds plain text, escape once at display
65
- `tabs.json` tab names and `project.json` `title` values are stored as **plain text**. The single correct escape happens at display time in `assets/js/design.js` `esc()` (and in `buildStub()`, which `htmlspecialchars()` the title into the generated `index.html`).
66
-
67
- `App_Design_Github::decodeEntities()` (one pass of `html_entity_decode` with `ENT_QUOTES | UTF-8`) enforces that invariant at two chokepoints:
68
- - **On read out of storage** — `loadTabs()`, `normalizeTabs()` (including the legacy scalar `tab`), and the project read's `title`.
69
- - **On names/titles read off a request** — `createTab()`, `renameTab()` (both old and new name), `deleteTab()`, `reorderTabs()`, `filterKnownTabs()`, `createProject()`, and `upload()` titles.
70
-
71
- Both sides are required: decoding only reads would make `renameTab()` compare a decoded `H&H` against a stored `H&amp;H` and fail with *"No such tab."*; decoding only writes would leave already-bad stored data permanently double-escaped on screen.
72
-
73
- **Self-healing:** every tab write rewrites `tabs.json` and each affected `project.json` in full, so pre-encoded stored values are re-persisted as plain text on the next rename/delete/reorder. No manual repair of the `forward` repo is needed.
74
-
75
- ### Actions & UX
76
- - **Publish a version** — uploads a self-contained HTML export as the next `vN/index.html`, appends a version entry, regenerates the stub, commits.
77
- - **Remove version** — collapses the version out of the main list; unlisted versions sit behind a per-project "Show N unlisted" collapsible.
78
- - **Unlist a project ("Remove")** — reversible `hidden` flag; files are not deleted.
79
- - **Hard-delete a whole project** — `ghDeletePaths()` reads the head tree recursively, rebuilds a fresh tree **excluding the project folder** (exact folder-boundary prefix match), commits, then PATCHes the ref. Offered alongside the reversible unlist.
80
- - **Set latest / toggle version / edit version / toggle project / set project tabs** — commit the relevant `project.json` (metadata-only edits do not rebuild the stub).
81
- - **Copy URL** and **Copy Claude Design prompt** — clipboard helpers.
82
- - **Single scroll region** — `#projects` is the only scroll area; header/tabbar are fixed, so switching tabs never shifts layout.
83
- - The Claude Design export **prompt modal** forces a dark `<pre>` background + light text with `!important` (the global light `pre` theme otherwise wins).
68
+ Tab names and project `title` are stored **plain**. The one escape is at display: `design.js` `esc()` and `buildStub()` (`htmlspecialchars()`). `App_Design_S3::decodeEntities()` (one `html_entity_decode`, `ENT_QUOTES | UTF-8`) runs at both chokepoints:
69
+ - on read from storage — `loadTabs()`, `normalizeTabs()` (incl. legacy `tab`), project `title`;
70
+ - on request input — `createTab()`, `renameTab()` (old + new), `deleteTab()`, `reorderTabs()`, `filterKnownTabs()`, `createProject()`, `upload()` titles.
71
+
72
+ Both are needed: decode-on-read only → `renameTab()` compares `H&H` to stored `H&amp;H` → *"No such tab."*; decode-on-write only → old bad data stays double-escaped. Every tab write rewrites values in full, so bad stored data heals on the next write.
73
+
74
+ ### Actions
75
+ - **Publish a version** — next `vN/index.html`, version entry, stub.
76
+ - **Adopt** — turns a legacy folder with only `index.html` into a managed project (`v1` = that page).
77
+ - **Remove version** / **Show N unlisted** — hidden versions collapse.
78
+ - **Unlist project ("Remove")** — reversible `hidden` flag; files stay (frees no storage).
79
+ - **Hard-delete project** — list `<project>/` keys → `DeleteObjects` (batches of 1000) → CloudFront invalidation of `/<project>/*` and `/<project>`. Refuses folders without `project.json`/`index.html`.
80
+ - **Set latest / toggle version / edit version / toggle project / set project tabs** — `project.json` write (metadata-only edits skip the stub).
81
+ - **Copy URL**, **Copy Claude Design prompt** — clipboard helpers. Prompt modal forces a dark `<pre>` with `!important`.
82
+ - `#projects` is the only scroll region.
84
83
 
85
84
  ### Hardening
86
- - `isValidProjectName()` guards on `setLatest` / `toggleVersion` / `editVersion` / `toggleProject` / `setProjectTabs`.
87
- - `upload()` **errors on corrupt project.json** instead of silently resetting history.
88
- - Inline `window.DESIGN_CONFIG` uses `JSON_HEX_*` flags.
89
- - Input length caps, version-id regex guard, HTML content sniff on upload; no client-facing leakage of GitHub/curl error detail.
85
+ - `isValidProjectName()` on every project action; `RESERVED_NAMES` blocked.
86
+ - `createProject()` / `upload()` refuse a name where a legacy `index.html` exists — adopt it instead.
87
+ - `upload()` errors on corrupt `project.json` instead of resetting history.
88
+ - `window.DESIGN_CONFIG` uses `JSON_HEX_*`; input length caps; version-id regex; HTML sniff on upload; AWS error detail is logged, never shown to the user.
90
89
 
91
- ### Upload size + timeout
92
- - Cap: `App_Design_Github::MAX_UPLOAD_BYTES` = 50 MB. GitHub warns on files >50 MB, blocks >100 MB.
93
- - php.ini (`.ebextensions/009_setup_phpini.config`): `upload_max_filesize=50M` / `post_max_size=55M`. **Keep these above `MAX_UPLOAD_BYTES`** — raise both together.
94
- - `ghApi` curl timeout = `GITHUB_TIMEOUT_SECONDS` (180s); a 50 MB file is ~67 MB as a base64 blob. If `json_encode` fails, `ghApi` logs and throws `RuntimeException` (never POSTs an empty body).
95
- - Apache has no `LimitRequestBody`; `memory_limit` 2G is enough.
90
+ ### Upload size
91
+ - `App_Design_S3::MAX_UPLOAD_BYTES` = 50 MB.
92
+ - php.ini (`.ebextensions/009_setup_phpini.config`): `upload_max_filesize=50M` / `post_max_size=55M`. **Keep these above `MAX_UPLOAD_BYTES`**; raise both together.
96
93
 
97
94
  ### Access control
98
- Behind Tools SSO. The page self-guards with `App_Auth::requireAuth([...])` and `App_Page::validateCrossSiteRequestForgery()`; the nav action is gated to the `TOGa Technology` and `Development Team` personas. This is the key improvement over the old `forward` tool, which had **no** authentication.
95
+ Tools SSO + `requireAuth` + CSRF check; nav gated to `TOGa Technology` and `Development Team` personas.
99
96
 
100
- ### GitHub token
101
- `App_Design_Github::token()` reads the token from `getenv()` **AND** `$_SERVER` **AND** `$_ENV`, then falls back to a `[design] github_token` config key for local dev. The EB environment property is named **`GITHUB_TOKEN`** (value not recorded here). It is never logged or echoed. Reading all three superglobals is required because php-fpm on AL2023 defaults to `clear_env=yes` — see the deploy workflow gotcha. Prefer a **fine-grained PAT** scoped to the `forward` repo's Contents R/W.
97
+ ### AWS credentials (values never recorded)
98
+ `credentials()` order:
99
+ 1. Env `DEMOS_AWS_ACCESS_KEY_ID` / `DEMOS_AWS_SECRET_ACCESS_KEY` — read from `getenv()` AND `$_SERVER` AND `$_ENV` (php-fpm `clear_env=yes`; see deploy workflow).
100
+ 2. Config `[design] aws_access_key_id` / `aws_secret_access_key` — local dev.
101
+ 3. SDK default chain — **prod uses this**: the EB instance role `ElasticBeanstalk-EC2-Instance-Profile` (shared prod EB role) has IAM policy **`togatech-demos-publisher`**: S3 Get/Put/Delete on bucket objects, ListBucket on the bucket, `cloudfront:CreateInvalidation` on the distribution.
102
+
103
+ Tools runs in the **PROD** account as EB env `tools-production`, app `Tools`, **us-east-1**, host `tools.togahub.com`. The bucket is in us-west-2 — the client region comes from `REGION`, not the host.
102
104
 
103
105
  ## Gotchas
104
- - **`curl_close()` is deprecated as of PHP 8.5** (and a no-op since 8.0). The 1.0 framework on PHP 8.5 promotes deprecations to `ErrorException`, which aborts the request — every GitHub call threw and the tool returned "Unexpected error" (0 projects). Fix: removed all `curl_close()` calls. Audit ported code for other deprecated-in-8.x calls.
105
- - **Contents API inlines only ≤1 MB** — read exports with the raw media type (see above).
106
- - **Publishing pushes to `forward`'s `_main`** and triggers EB auto-deploy; no staging.
107
- - **Double-escaped names (`H&amp;H` shown literally, rename fails with "No such tab.")** — caused by values that arrived **already HTML-encoded** and were then correctly escaped once more at display. The render path was never wrong; no current code path produces the encoding (`createTab`/`createProject` and the JS transport `window.prompt` → `URLSearchParams` → `$postField` trim/`mb_substr` are clean), so encoded values entered from outside — a paste from rendered HTML, or a pre-fix build. Fixed defensively with `decodeEntities()` on both the read and request sides (see above). Generated project stubs were affected identically, because `buildStub()`'s single correct `htmlspecialchars()` was a *second* escape on a pre-encoded title; storing titles plain fixes the stubs too.
108
- - **Upload fails on a big export (e.g. 28 MB)** — hit the old 25 MB cap in php.ini and `MAX_UPLOAD_BYTES`. Fixed 2026-10-08 (now 50 MB). Change both places together.
109
- - Never hardcode the token in tracked source — a leaked `ghp_`/PAT must be rotated.
106
+ - **`curl_close()` is fatal on PHP 8.5** in the 1.0 framework (deprecation → `ErrorException`). Removed in this tool; audit ported code for other deprecated calls.
107
+ - **Double-escaped names (`H&amp;H`, rename says "No such tab.")** — values arrived already HTML-encoded (paste from rendered HTML / pre-fix build). Fixed by `decodeEntities()` on both sides (above). Stubs were affected the same way.
108
+ - **Upload limit** — raise `MAX_UPLOAD_BYTES` and php.ini together.
109
+ - **Locally, the AWS SDK prints `syntax error, unexpected '='` warnings** while parsing a local `~/.aws` config file. Harmless; not seen in prod.
110
+ - **Do not go back to storing demos in a git repo deployed to EB.** That is why `forward` broke (EB 500 MB bundle cap) — see [Static Demo Hosting](../../../standalone/apps/forward/features/static-demo-hosting.md).
111
+ - Never hardcode AWS keys in tracked source.
110
112
 
111
113
  ## Related
112
114
  - [Tools Architecture](../architecture.md)
113
115
  - [Static Demo Hosting](../../../standalone/apps/forward/features/static-demo-hosting.md)
114
116
  - [Deploying Tools to Elastic Beanstalk (AL2023)](../workflows/deploy-to-elastic-beanstalk-al2023.md)
117
+ - [GitHub Audit](./github-audit.md) — used to share this tool's GitHub token; now has its own
118
+
119
+ ## Change history
120
+ - 2026-10-09 — Storage moved from the `forward` GitHub repo to S3 `togatech-demos` + CloudFront; `App_Design_Github` → `App_Design_S3`; conditional writes; prod access via instance role (TRUE-82616) (jcardinal)
121
+ - 2026-10-08 — Upload cap 25 MB → 50 MB (php.ini + `MAX_UPLOAD_BYTES`) (jcardinal)
122
+ - 2026-07-24 — Admin UI moved from `forward` (no auth) into SSO-protected Tools (jcardinal)
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-09
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - tools/_/app/github/audit.php
@@ -21,24 +21,24 @@ related:
21
21
  - ../../test/features/github-audit-script.md
22
22
  ---
23
23
 
24
- Internal monthly code-churn report at `/developers/github-audit` (lines added/removed across every `agilantsolutions` repo for one month); open for the two-phase run/progress model, the shared-token rate-limit budget gate, or the SSRF guard.
24
+ Internal monthly code-churn report at `/developers/github-audit` (lines added/removed across every `agilantsolutions` repo for one month); open for the two-phase run/progress model, the rate-limit budget gate, or the SSRF guard.
25
25
 
26
26
  ## Summary
27
27
 
28
28
  An internal report at **`/developers/github-audit`** (Developers nav group, personas `['Development Team','TOGa Technology']`) that shows, for one calendar month, the **lines added/removed across every repo in the `agilantsolutions` GitHub org**, plus commit count, unique authors, and active repo count. Built so the team gets a monthly code-churn report without leaving GitHub.
29
29
 
30
- It is the app port of the standalone [`test/team/github_audit.php`](../../test/features/github-audit-script.md) script (built first, then moved into Tools). The engine reuses the **Design tool's GitHub plumbing** — same token, same log-detail/return-generic error pattern.
30
+ It is the app port of the standalone [`test/team/github_audit.php`](../../test/features/github-audit-script.md) script (built first, then moved into Tools). Uses the same log-detail/return-generic error pattern as the Design tool.
31
31
 
32
32
  ## How it works
33
33
 
34
34
  ### Key files / entry points
35
- - `_/app/github/audit.php` — class **`App_Github_Audit`**: a static, read-only GitHub REST engine (not a Controller/Model). Mirrors the sibling `App_Design_Github` static-engine pattern.
35
+ - `_/app/github/audit.php` — class **`App_Github_Audit`**: a static, read-only GitHub REST engine (not a Controller/Model). Same static-engine pattern as `App_Design_S3`.
36
36
  - `mvc/developers/github-audit/get.php` — the page: native `<input type="month">` picker (defaults to last month), a Run button, a progress bar, and results (summary cards + per-repo table). Themed with `--c-*` tokens (light/dark).
37
37
  - `mvc/developers/github-audit/post.php` — AJAX JSON endpoint. `requireAuth` + `App_Page::validateCrossSiteRequestForgery()`; allowlisted actions only.
38
38
  - `_/app/nav.php` — one nav action under the `developers` group.
39
39
 
40
- ### Shared token (depends on the Design tool)
41
- The audit engine does **not** define its own token config. It reuses `App_Design_Github::token()` — the `GITHUB_TOKEN` env property first, then the `[design] github_token` config fallback. So the GitHub token is **shared** between the Design tool and the audit tool, which drives the concurrency lock below. Value never recorded, logged, or echoed. See [Design Demo Admin › GitHub token](./design-demo-admin.md).
40
+ ### GitHub token
41
+ `App_Github_Audit::token()` resolves its own token: EB env `GITHUB_TOKEN` from `getenv()` AND `$_SERVER` AND `$_ENV` (php-fpm `clear_env=yes`), then config `[design] github_token` for local dev. Value never recorded, logged, or echoed. The Design tool no longer uses GitHub (moved to S3 2026-10-09), so this tool is the token's only Tools user.
42
42
 
43
43
  ### Accuracy — per-commit exact counting
44
44
  To get exact line counts, the engine lists **each commit** in the month window (`since`/`until`), then fetches **each commit's stats**. Merge commits are **skipped** (they would double-count), and forks are **skipped**.
@@ -59,7 +59,7 @@ The client accumulates totals and authors across the per-repo responses and rend
59
59
  - **Budget gate** — `const MIN_REMAINING_TO_RUN = 1000;` sets the floor. Enforced at two points, both throwing a user-safe (`ERR_USER_SAFE`) message:
60
60
  - **Pre-flight:** `App_Github_Audit::assertBudget()` calls `GET /rate_limit` (which does **not** count against the rate limit) at the start of a run. If the shared token's core `remaining` is **at or below 1000**, it refuses to start and the scan never begins.
61
61
  - **Mid-run:** `assertRateLimitOk()` **stops** the run once `x-ratelimit-remaining` drops to 1000 — read from headers already returned, so no extra calls — and shows partial results.
62
- - This replaced the earlier tiny `RATE_LIMIT_FLOOR=25` safety. Rationale: the GitHub token is **shared with the Design tool**, so the audit must leave a healthy budget for other work.
62
+ - This replaced the earlier tiny `RATE_LIMIT_FLOOR=25` safety. Rationale: the org rate limit is shared with other GitHub users of the token, so the audit leaves a healthy budget.
63
63
 
64
64
  ### Live API-call + budget counter
65
65
  - `App_Github_Audit::callCount()` — calls made in this PHP request.
@@ -68,10 +68,10 @@ The client accumulates totals and authors across the per-repo responses and rend
68
68
  The page shows a live line: `API calls this run: N · GitHub budget: X of 5,000 left`, and the total in the results caption.
69
69
 
70
70
  ### Per-session concurrency lock (shared-token protection)
71
- `post.php` takes an **APCu lock** (`apcu_add`, key `gha_audit_lock_<session_id>`, 300s TTL) so only one audit request runs at a time per session. Reason: the GitHub token is **shared** with the Design tool, so parallel scans would burn the org's rate limit for everyone. If APCu is unavailable the lock is skipped cleanly. **Known limit:** APCu is per-EB-instance, so the lock is **per-instance**, not global.
71
+ `post.php` takes an **APCu lock** (`apcu_add`, key `gha_audit_lock_<session_id>`, 300s TTL) so only one audit request runs at a time per session. Reason: parallel scans would burn the org's rate limit for everyone. If APCu is unavailable the lock is skipped cleanly. **Known limit:** APCu is per-EB-instance, so the lock is **per-instance**, not global.
72
72
 
73
73
  ### Error sanitizing
74
- Only a **user-safe** `RuntimeException` (the rate-limit notice, thrown with code `App_Github_Audit::ERR_USER_SAFE`) reaches the browser. Every other `RuntimeException` / `Throwable` is logged and returned as a **generic** message — GitHub's raw text and repo names never surface. Same log-detail / return-generic pattern as `App_Design_Github::commitError()`.
74
+ Only a **user-safe** `RuntimeException` (the rate-limit notice, thrown with code `App_Github_Audit::ERR_USER_SAFE`) reaches the browser. Every other `RuntimeException` / `Throwable` is logged and returned as a **generic** message — GitHub's raw text and repo names never surface. Same log-detail / return-generic pattern as the Design tool.
75
75
 
76
76
  ### Input validation & SSRF guard
77
77
  - **Month:** must match `^\d{4}-\d{2}$` **and** month number 01–12. The regex alone accepts `2026-99`, which `DateTime::createFromFormat` silently overflows.
@@ -87,10 +87,12 @@ Behind Tools SSO. `get.php` and `post.php` self-guard with `requireAuth` + `App_
87
87
  - **The `hidden` attribute is beaten by a class `display:` rule.** The results/spinner box had the HTML `hidden` attribute but still showed on load (looked like the tool auto-ran), because an author `display:flex` class rule beats the UA `[hidden]{display:none}`. Fix: a scoped guard `.<scope> [hidden] { display:none !important; }` (or `:not([hidden])` on the display rule). General server-rendered-UI trap.
88
88
  - **`curl_close()` is fatal on PHP 8.5** in the 1.0 framework (deprecation → `ErrorException` → aborted request). The audit engine avoids `curl_close()` — same rule as the Design tool. See [Tools architecture](../architecture.md) and framework-rules.
89
89
  - **APCu lock is per-EB-instance**, so the single-run guard is per-instance, not org-wide.
90
- - **Shared rate-limit budget** — this tool and the Design tool draw on the same GitHub token, so a big audit eats into the Design tool's budget and vice versa.
91
90
 
92
91
  ## Related
93
92
  - [Tools Architecture](../architecture.md)
94
- - [Design Demo Admin](./design-demo-admin.md) — source of the shared GitHub token and error pattern
93
+ - [Design Demo Admin](./design-demo-admin.md) — same error pattern
95
94
  - [Front-End Standards](../../../standards/frontend.md)
96
95
  - [github_audit.php standalone script](../../test/features/github-audit-script.md)
96
+
97
+ ## Change history
98
+ - 2026-10-09 — `token()` now resolves its own `GITHUB_TOKEN`; no longer borrows `App_Design_Github::token()` (deleted when Design moved to S3, TRUE-82616) (jcardinal)
@@ -34,7 +34,7 @@ Replaces `test/team/2.0 deployment/generate_client_sql.php`. Development Team pe
34
34
  3. Lists are dynamic — no tools commit when clients change:
35
35
  - **All Clients / Client Logs / Client Archives** — live read of `Core.Clients` joined to `Core.Databases` on `clientDatabaseId` / `logDatabaseId` / `archiveDatabaseId`, via config `[database_toga2core]`. Keeps a DB only if `EXISTS` a `Core.DatabaseHosts` row (INNER JOIN `Environments` on `slug`) for the `ENVIRONMENT` env var (tools is production-only: `production`). Missing `ENVIRONMENT` → user-safe error, never an unfiltered list. Why: `Core.Clients` has never-built clients (see [per-client DB gotcha](../../../../2.0/apps/_underscore/features/per-client-database-connections.md)). `isActive` flag rejected: inactive ≠ no DB, flags drift, and it is a Core schema change.
36
36
  - **Module lists** — one per folder under `dbchanges2/_modules` (branch `_main`). A client is in the list when `Client_<Name>/_modules.txt` names the module (case-insensitive); DB name = client folder name. GitHub GraphQL, 2 calls (folder list, then all `_modules.txt` as aliased blobs). APCu cache 5 min + 7-day "last good" fallback shown with a warning. Warns when a module client is not in `Core.Clients`; drops clients whose client DB is not built. One retry on a dropped connection; log names the failed step (`folder list` / `_modules.txt read`).
37
- 4. Env vars `GITHUB_TOKEN` and `ENVIRONMENT` read by shared `envValue()` (`getenv` + `$_SERVER` + `$_ENV`, because php-fpm `clear_env`). GitHub token: **only** `GITHUB_TOKEN`. No config fallback (developer decision). Own curl, not `App_Design_Github`. Needs read on `agilantsolutions/dbchanges2`; set as an EB environment property in prod.
37
+ 4. Env vars `GITHUB_TOKEN` and `ENVIRONMENT` read by shared `envValue()` (`getenv` + `$_SERVER` + `$_ENV`, because php-fpm `clear_env`). GitHub token: **only** `GITHUB_TOKEN`. No config fallback (developer decision). Own curl. Needs read on `agilantsolutions/dbchanges2`; set as an EB environment property in prod.
38
38
  5. Every list returns a `skipped` array, shown on the page ("Skipped N (database not built — no DatabaseHosts row): …").
39
39
  6. Every DB/folder name is checked against `^[A-Za-z0-9_]{1,64}$` before it goes into GraphQL or a `USE` line. Page writes via `textContent`/`.value` only.
40
40
 
@@ -148,6 +148,11 @@ All tested 2026-10-08:
148
148
  ticket-branch commits' `/commit` notes + the diff (risk from `skills/finish/review/pr-risk.js`).
149
149
  An open PR gets its description rebuilt (`gh pr edit --body-file`). The GitHub Action files
150
150
  (`.github/workflows/pr-summary.yml`, `ci/pr-review/`) stay in the repo unused.
151
+ - **Session title.** After verify, `/start` saves `<TICKET>: <2-4 word short title>` with
152
+ `start-helper.js set-title` (`sessionTitle` in the session file) and shows `/rename <title>`
153
+ to paste. The SessionStart hook also returns it as `sessionTitle` when the developer has not
154
+ named the session. Hook titles only work at SessionStart (per docs); VS Code support is
155
+ unconfirmed; `/rename` works everywhere.
151
156
 
152
157
  ## Change history
153
158
 
@@ -169,6 +174,9 @@ All tested 2026-10-08:
169
174
  - 2026-10-09 — PM feedback: PM comment is plain bullets for a non-technical PM; Apps, Clients,
170
175
  PRs removed; empty sections left out; field changes as plain sentences with the reason. (jcardinal)
171
176
 
177
+ - 2026-10-09 — TRUE-82605: `/start` names the session `<TICKET>: <short title>` (set-title +
178
+ SessionStart `sessionTitle`; `/rename` to paste). (jcardinal)
179
+
172
180
  ## Related
173
181
 
174
182
  - [Ticket-branch creation safety](./ticket-branch-creation-safety.md)
@@ -4,4 +4,4 @@
4
4
  |-----|---------|
5
5
  | [Forwarder Architecture](architecture.md) | Tiny standalone PHP redirect app for TOGA's branded short domains (Apache RewriteMap + light `index.php`); open for the two redirect layers, deploy, decrypt sch |
6
6
  | [Encrypted-Link Handler](features/encrypted-link-handler.md) | Forwarder `index.php` handler for redirect domains whose URL path carries an encrypted token (PCMatic/Newegg/`forward.*` agent downloads); open for config + dec |
7
- | [Static Demo Hosting](features/static-demo-hosting.md) | Serve self-contained static HTML demos on Forwarder under a clean URL (e.g. |
7
+ | [Static Demo Hosting](features/static-demo-hosting.md) | How `demo.togatech.com/<project>/vN/` static demos are hosted: S3 `togatech-demos` + CloudFront (prod account) since 2026-10-09, no longer `forward`; open for t |
@@ -6,62 +6,60 @@ project: Forwarder
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-09
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - forward/.htaccess
13
- - forward/togadesk/index.html
13
+ - tools/_/app/design/s3.php
14
14
  related:
15
15
  - standalone/apps/forward/architecture.md
16
- - standalone/apps/forward/features/design-demo-admin.md
16
+ - 1.0/apps/tools/features/design-demo-admin.md
17
17
  ---
18
18
 
19
- Serve self-contained static HTML demos on Forwarder under a clean URL (e.g. `demo.togatech.com/togadesk`) with no redirect/PHP; open to add a demo or export a Claude Design page.
19
+ How `demo.togatech.com/<project>/vN/` static demos are hosted: S3 `togatech-demos` + CloudFront (prod account) since 2026-10-09, no longer `forward`; open for the AWS setup, cache rules, rollback to EB, or the Claude Design export prompt.
20
20
 
21
21
  ## Summary
22
22
 
23
- A lightweight way to host self-contained static HTML pages (demos, exported designs, download landing pages) on the Forwarder app under a clean URL — e.g. `https://demo.togatech.com/togadesk`. The hostname stays in the browser and the page is served straight off disk with no redirect and no PHP compute. This is how we publish exported **Claude Design** mockups for stakeholders to view without the Claude editor / chat sidebar. First real use: the TOGa Desk design demo at `togadesk/`.
23
+ Self-contained static HTML demos (mostly exported **Claude Design** pages) served at `https://demo.togatech.com/<project>` with versioned `/<project>/vN/`. Since **2026-10-09** (TRUE-82616) they live in S3 bucket **`togatech-demos`** behind CloudFront — **not** in the `forward` repo. URLs did not change. Publishing is done by [Design Demo Admin](../../../../1.0/apps/tools/features/design-demo-admin.md) in Tools.
24
24
 
25
- ## How it works
26
-
27
- **Key files / entry points:**
28
-
29
- - `forward/.htaccess` — a guarded static-serving rule placed **before** the `forward.ini` RewriteMap rules.
30
- - `forward/<name>/index.html` — one directory per demo at the repo root (e.g. `forward/togadesk/index.html`). Apache serves it directly.
31
- - A `demo.togatech.com` CNAME pointed at the Forwarder Elastic Beanstalk environment.
32
-
33
- The rule in `.htaccess`:
34
-
35
- ```apache
36
- # Static demo hosting (e.g. demo.togatech.com/togadesk)
37
- RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
38
- RewriteRule ^([^/]+)/?$ /$1/index.html [L]
39
- ```
25
+ Why moved: the `forward` repo grew to 757 MB (toga-website alone 345 MB over 24 versions). CodePipeline `Forward-Production` (legacy account) failed every deploy from 2026-10-08 with *"The artifact is empty or exceeds the maximum size supported by AWS Elastic Beanstalk"* (EB 500 MB bundle cap), so new demos 404'd.
40
26
 
41
- - Matches a single path segment (`/togadesk` or `/togadesk/`) and internally serves `<docroot>/<segment>/index.html` — **no external redirect**, so the URL the user typed stays in the address bar.
42
- - The `RewriteCond … -f` guard means the rule only fires when that `index.html` actually exists, so it never interferes with the existing `forward.ini` redirect map; unmatched paths fall through to the map and then `index.php` exactly as before.
43
- - It sits before the RewriteMap block on purpose, so a real demo file always wins over a map lookup.
27
+ ## How it works
44
28
 
45
- **Adding a new demo** is drop-in: create `forward/<name>/index.html`, commit, deploy the EB app → `demo.togatech.com/<name>` works immediately. No code or `.htaccess` change is needed per demo.
29
+ All in the **PROD** account (654654170868).
46
30
 
47
- **Self-serve + versioning.** The design team no longer commits by hand: the [Design Demo Admin tool](design-demo-admin.md) at `/design` publishes exports via the GitHub API and adds **sequential `/vN/` versioning** on top of this static layout (a `project.json` manifest plus an auto-generated latest-pointer stub at the bare `/<project>`). The nested `/<project>/<vN>/` URLs are served by Apache's normal directory index (a `DirectoryIndex index.html index.php` line in `.htaccess`); the single-segment static-demo rewrite rule above does not match two segments.
31
+ - **S3 `togatech-demos`** (us-west-2): private, Block Public Access, BucketOwnerEnforced, SSE-S3, versioning ON. Lifecycle: noncurrent versions expire after 90 days; incomplete multipart uploads abort after 7 days. Root `404.html`. Bucket policy allows only this distribution (`AWS:SourceArn`).
32
+ - **CloudFront `E2XN9C2HZYWW3D`** (`dxd7aolawne76.cloudfront.net`): alias `demo.togatech.com`, existing ACM cert `*.togatech.com` (us-east-1), OAC `togatech-demos-oac`, PriceClass_100, redirect-to-https, cache policy **CachingOptimized**. Custom errors 403/404 → `/404.html` (status 404).
33
+ - **CloudFront Function `demo-togatech-dir-index`** (viewer-request): `/x/` → `/x/index.html`; extension-less `/x` → 301 to `/x/` (so the stub's relative `./vN/` links resolve).
34
+ - **Route 53** zone `togatech.com` (prod): `demo.togatech.com` = A + AAAA alias → the distribution (was a CNAME to `agilant-forward-prod`).
35
+ - **Cache-Control per object** (set by the admin tool): `vN/index.html` `public, max-age=86400`; stub `<project>/index.html`, `project.json`, `tabs.json` `public, max-age=60`. A normal publish needs no invalidation; hard delete invalidates `/<project>/*` and `/<project>`.
36
+ - **Migration (done):** 24 projects (folders with `project.json`) + `tabs.json` = 153 files / 728 MB copied with the same keys, checked byte-for-byte (sha256) through CloudFront, then DNS cutover, then removed from `forward`.
48
37
 
49
- This is distinct from the older subdomain-based **local directory handler** in `index.php` (`sos.*` → `/sos/`), which runs only on a map miss and issues a redirect. The static-demo rule is path-based, redirect-free, and runs in Apache before PHP. See the [architecture](../architecture.md) doc.
38
+ ### Rollback demo.togatech.com to forward EB
39
+ Only works if the demo files are put back in `forward` and it deploys under 500 MB.
40
+ 1. Restore the demo folders + `tabs.json` into `forward` (copy from S3 or from git history) and confirm the EB bundle is under 500 MB.
41
+ 2. Deploy `forward` via `Forward-Production` and confirm it succeeds.
42
+ 3. In Route 53 zone `togatech.com` (prod), reverse the change batch: delete the A + AAAA alias records for `demo.togatech.com`, create the CNAME → `agilant-forward-prod` EB host.
43
+ 4. Confirm `https://demo.togatech.com/<project>` loads.
50
44
 
51
- **Exporting a Claude Design page for hosting.** Demos are typically Claude Design artifacts exported as a **single self-contained HTML file**. A generic prompt to give inside the Claude Design conversation (no design-specific names) that yields a downloadable file:
45
+ ### Exporting a Claude Design page
46
+ Prompt to give inside the Claude Design conversation (also in the admin's "Copy Claude Design prompt"):
52
47
 
53
48
  > Package this design as a single, fully self-contained HTML file I can host anywhere, and give it to me as a downloadable file (not a copy/paste code block). Requirements: one file with no build step or local assets; inline all CSS and JS; embed images/fonts as `data:` URIs or absolute `https://` URLs (no root-relative `/...` or relative `./...` paths, and no `<base>` tag); remove anything tied to the editing environment (no sidebar/chat refs, no live data calls back to the session — replace dynamic data with static placeholder content); it must render when opened directly in a browser; set a sensible `<title>` and viewport meta. Then save it as a downloadable `.html` file and give me the download link.
54
49
 
55
- Before hosting, verify the export has: no `<base>` tag, no root-relative/relative asset paths, no `fetch`/XHR/API calls back to a live backend (a detached demo must be static), and that it renders when opened directly by double-clicking. Public `https://` CDN dependencies (Font Awesome, Lucide, Tailwind CDN, Google Fonts) are fine — but the page then needs internet to render fully.
50
+ Before hosting, check: no `<base>` tag, no root-relative/relative asset paths, no `fetch`/XHR to a live backend, renders when double-clicked. Public CDN dependencies (Font Awesome, Tailwind CDN, Google Fonts) are fine but need internet.
56
51
 
57
52
  ## Gotchas
58
-
59
- - **Bare domain root 404s.** `demo.togatech.com/` (no demo name) has no `forward.ini` entry, so it falls through to `index.php` → 404. Add a `forward.ini` line if the root should redirect somewhere.
60
- - **The rule matches any host, not just `demo.togatech.com`.** It is guarded by the file existence check, so `anyhost/togadesk` would also serve the demo. Acceptable today since only the `demo` CNAME is meant to be used; keep demo directory names from colliding with real first-path-segment redirect targets.
61
- - **CDN dependencies need internet.** Exported pages that link CDN CSS/JS won't render fully offline. Fine for hosted demos.
62
- - **Large files are fine but bloat the repo.** Exports with base64-inlined assets can be multi-MB (the TOGa Desk demo is ~4 MB). They deploy with the EB app like any other file.
53
+ - **Never use the managed cache policy `UseOriginCacheControlHeaders` with an S3 origin.** It forwards the `Host` header, so S3 returns NotFound for every key. Use `CachingOptimized`.
54
+ - **EB has a 500 MB bundle cap**, and the CodePipeline EB deploy action **ignores `.ebignore`**. Large static assets do not belong in an EB-deployed repo.
55
+ - **"Unlist" frees no storage** — files stay. Only hard delete removes them (S3 keeps noncurrent versions 90 days).
56
+ - **Extension-less `/x` must 301 to `/x/`**, or the stub's relative `./vN/` links resolve against the root.
57
+ - `forward/.htaccess` still has the old static-demo rewrite + `DirectoryIndex` lines; harmless now that no demo folders are in the repo.
63
58
 
64
59
  ## Related
65
-
60
+ - [Design Demo Admin](../../../../1.0/apps/tools/features/design-demo-admin.md)
66
61
  - [Forwarder Architecture](../architecture.md)
67
- - [Encrypted-Link Handler](encrypted-link-handler.md)
62
+
63
+ ## Change history
64
+ - 2026-10-09 — Hosting moved from `forward` EB to S3 `togatech-demos` + CloudFront (prod); DNS cut over; demo folders removed from `forward` (TRUE-82616) (jcardinal)
65
+ - Earlier — demos served from `forward/<name>/index.html` via the `.htaccess` rule, versioned `/vN/` added by the admin tool (jcardinal)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.991",
3
+ "version": "1.0.992",
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",
@@ -50,7 +50,27 @@ function teamSessions(teamRepo) {
50
50
  } catch { return []; }
51
51
  }
52
52
 
53
+ const MAX_TITLE_LENGTH = 60;
54
+
55
+ function readStdin() {
56
+ try { return JSON.parse(fs.readFileSync(0, 'utf8') || '{}') || {}; } catch { return {}; }
57
+ }
58
+
59
+ // Saved "<TICKET>: <short>" title from /start (start-helper.js set-title), or ''.
60
+ function savedTitle(sessionId) {
61
+ const sid = String(sessionId || '').replace(/[^A-Za-z0-9_-]/g, '');
62
+ if (!sid) return '';
63
+ const dir = process.env.TOGA_SESSIONS_DIR || path.join(require('os').homedir(), '.claude', 'toga-sessions');
64
+ try {
65
+ const t = JSON.parse(fs.readFileSync(path.join(dir, sid + '.json'), 'utf8')).sessionTitle;
66
+ return typeof t === 'string' ? t.trim().slice(0, MAX_TITLE_LENGTH) : '';
67
+ } catch { return ''; }
68
+ }
69
+
53
70
  function main() {
71
+ const input = readStdin();
72
+ const lines = [];
73
+ const log = (line) => lines.push(line == null ? '' : String(line));
54
74
  const cwd = process.cwd();
55
75
  const now = new Date().toLocaleDateString('en-US', {
56
76
  weekday: 'long',
@@ -59,19 +79,19 @@ function main() {
59
79
  day: 'numeric',
60
80
  });
61
81
 
62
- console.log('');
63
- console.log('================================================');
64
- console.log(' TOGA Team Knowledge — ' + now);
65
- console.log('================================================');
66
- console.log('');
67
- console.log('Tip of the day:');
68
- console.log(' ' + getTip());
69
- console.log('');
82
+ log('');
83
+ log('================================================');
84
+ log(' TOGA Team Knowledge — ' + now);
85
+ log('================================================');
86
+ log('');
87
+ log('Tip of the day:');
88
+ log(' ' + getTip());
89
+ log('');
70
90
 
71
91
  if (checkUnfinishedSession(cwd)) {
72
- console.log('Unfinished session detected.');
73
- console.log(' Run /session-resume latest to reload your previous session state.');
74
- console.log('');
92
+ log('Unfinished session detected.');
93
+ log(' Run /session-resume latest to reload your previous session state.');
94
+ log('');
75
95
  }
76
96
 
77
97
  // Surface team-shared sessions (best-effort, time-boxed — never blocks startup).
@@ -80,16 +100,21 @@ function main() {
80
100
  if (sessions.length) {
81
101
  const newest = sessions[0];
82
102
  const fresh = sessions.filter(s => !s.stale).length;
83
- console.log(`${sessions.length} team session(s) available${fresh < sessions.length ? ` (${sessions.length - fresh} stale)` : ''}.`);
84
- console.log(` Newest: ${newest.id} by ${newest.author || 'unknown'} — "${newest.title || ''}"`);
85
- console.log(' Run /session-resume <id> to continue one, or /session-resume list to see all.');
86
- console.log('');
103
+ log(`${sessions.length} team session(s) available${fresh < sessions.length ? ` (${sessions.length - fresh} stale)` : ''}.`);
104
+ log(` Newest: ${newest.id} by ${newest.author || 'unknown'} — "${newest.title || ''}"`);
105
+ log(' Run /session-resume <id> to continue one, or /session-resume list to see all.');
106
+ log('');
87
107
  }
88
108
 
89
- console.log('Ready. Run /start TRUE-1234 (your ClickUp ticket id) to prime context for your work session.');
90
- console.log('');
109
+ log('Ready. Run /start TRUE-1234 (your ClickUp ticket id) to prime context for your work session.');
110
+ log('');
91
111
 
112
+ const out = { hookEventName: 'SessionStart', additionalContext: lines.join('\n') };
113
+ // Never override a name the developer set.
114
+ const title = String(input.session_title || '').trim() ? '' : savedTitle(input.session_id);
115
+ if (title) out.sessionTitle = title;
116
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: out }) + '\n');
92
117
  process.exit(0);
93
118
  }
94
119
 
95
- main();
120
+ try { main(); } catch { process.exit(0); }
@@ -94,6 +94,18 @@ customId is not the session ticket, the session file is missing, the internal id
94
94
  or the url is not `https://app.clickup.com/...`. On a refusal, show the error and stop. On
95
95
  `{ok:true,...}` the gate moves to the primer stage.
96
96
 
97
+ Then name the session. Make a short title: `<T>: <2-4 plain words from the ticket title>`
98
+ (e.g. `TRUE-82605: ClickUp workflow`, `TRUE-81379: Supply filter speed`). Drop "Client - "
99
+ prefix words only if needed to stay short; never "TOGa". Max 60 characters. Save it:
100
+
101
+ ```
102
+ node ".../start-helper.js" set-title --session <sid> --title "<T>: <short>"
103
+ ```
104
+
105
+ Then tell the developer in one line: "Paste this to name the session: `/rename <title>`".
106
+ The SessionStart hook also offers it as the session title on resume/compact, only when the
107
+ developer has not named the session.
108
+
97
109
  ## Step 4 — Ticket repos and branches
98
110
 
99
111
  1. **Repo list from the ticket's `application` values:**
@@ -231,6 +243,7 @@ kickoff Step 6: planner -> implement -> reviewers; auto-review rules apply):
231
243
 
232
244
  ```
233
245
  Ticket: <T> — <title> (<status>, <list>)
246
+ Session: <session title> (paste: /rename <session title>)
234
247
  Apps: <repos> (core: <core>)
235
248
  Clients: <clients>
236
249
  Task type: <type>
@@ -63,6 +63,8 @@ const GIT_NETWORK_TIMEOUT_MS = 120000;
63
63
  const MAX_ERROR_CHARS = 500;
64
64
  const TICKET_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
65
65
  const KB_REMOTE_RE = /(agilantsolutions|togatech)\/claude(\.git)?\/?$/i;
66
+ const MAX_TITLE_LENGTH = 60;
67
+ const CONTROL_CHARS_RE = /[\u0000-\u001f\u007f]+/g;
66
68
  const DEFAULT_BRANCH_GUESSES = ['_production', 'main', 'master', '_main'];
67
69
 
68
70
  /* ---------------- args + output ---------------- */
@@ -342,6 +344,22 @@ function cmdVerify(args) {
342
344
  print(Object.assign({ ok: true }, s));
343
345
  }
344
346
 
347
+ /* Session display title "<TICKET>: <short>". The SessionStart hook offers it as sessionTitle. */
348
+ function cmdSetTitle(args) {
349
+ const r = requireSession(args);
350
+ if (!r) return;
351
+ if (typeof args.title !== 'string') return fail('missing --title "<TICKET>: <short title>"');
352
+ const title = args.title.replace(CONTROL_CHARS_RE, ' ').replace(/\s+/g, ' ').trim();
353
+ const prefix = String(r.s.ticket || '').toUpperCase() + ': ';
354
+ if (!r.s.ticket || !title.toUpperCase().startsWith(prefix) || title.length <= prefix.length) {
355
+ return fail('title must start with "' + prefix + '" and have a short name after it');
356
+ }
357
+ if (title.length > MAX_TITLE_LENGTH) return fail('title is longer than ' + MAX_TITLE_LENGTH + ' characters');
358
+ r.s.sessionTitle = prefix + title.slice(prefix.length);
359
+ writeSession(r.sid, r.s);
360
+ print({ ok: true, sessionId: r.sid, ticket: r.s.ticket, sessionTitle: r.s.sessionTitle });
361
+ }
362
+
345
363
  /* Secret scan for text Claude is about to post (PR body, PM comment). Prints hit TYPES only. */
346
364
  function cmdScan(args) {
347
365
  if (typeof args.file !== 'string') { fail('missing --file <path>'); process.exitCode = SCAN_ERROR_EXIT; return; }
@@ -525,8 +543,9 @@ function main() {
525
543
  case 'repos-with-work': return cmdReposWithWork(args);
526
544
  case 'create-branch': return cmdCreateBranch(args);
527
545
  case 'scan': return cmdScan(args);
546
+ case 'set-title': return cmdSetTitle(args);
528
547
  default:
529
- return fail('usage: start-helper.js <get|verify|branch-check|create-branch|override-branch|latest-for|repos-with-work|scan> [--flags]');
548
+ return fail('usage: start-helper.js <get|verify|branch-check|create-branch|override-branch|set-title|latest-for|repos-with-work|scan> [--flags]');
530
549
  }
531
550
  }
532
551