@rashidee/co2 1.3.15 → 1.3.17

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.
Binary file
Binary file
package/dist/index.js CHANGED
@@ -6809,7 +6809,7 @@ var {
6809
6809
  // package.json
6810
6810
  var package_default = {
6811
6811
  name: "@rashidee/co2",
6812
- version: "1.3.15",
6812
+ version: "1.3.17",
6813
6813
  description: "Compound Context Studio \u2014 self-hosted team context authoring for CO2 projects",
6814
6814
  type: "module",
6815
6815
  license: "MIT",
@@ -14735,6 +14735,10 @@ var cicdSetups = sqliteTable(
14735
14735
  // project_configs.updatedAt at generation
14736
14736
  appPort: integer("app_port"),
14737
14737
  // last spawned port of the generated app
14738
+ // Guard token issued to the last spawned app (also kept in memory). Persisted so an app
14739
+ // that outlives a studio restart still passes /api/cicd-guard — otherwise its develop
14740
+ // gate reads "studio unreachable" and every control stays disabled until a manual bounce.
14741
+ guardToken: text("guard_token"),
14738
14742
  createdBy: integer("created_by").notNull(),
14739
14743
  // soft ref → users.id
14740
14744
  createdAt: text("created_at").notNull(),
@@ -22015,6 +22019,46 @@ function stopCicdApp(projectId) {
22015
22019
  }
22016
22020
 
22017
22021
  // ../server/src/features/cicd/app-proxy.ts
22022
+ var PROXY_BASE_SHIM = `<script data-co2-proxy-base-shim>(function () {
22023
+ var m = location.pathname.match(/^(.*?\\/app-proxy)(?:\\/|$)/)
22024
+ if (!m) return
22025
+ var prefix = m[1]
22026
+ function reroot(p) {
22027
+ return p.charAt(0) === '/' && p.charAt(1) !== '/' && p.indexOf(prefix + '/') !== 0 && p !== prefix ? prefix + p : p
22028
+ }
22029
+ var origFetch = window.fetch.bind(window)
22030
+ window.fetch = function (input, init) {
22031
+ if (typeof input === 'string') input = reroot(input)
22032
+ else if (input instanceof URL && input.host === location.host)
22033
+ input = new URL(reroot(input.pathname) + input.search + input.hash, location.origin)
22034
+ return origFetch(input, init)
22035
+ }
22036
+ var OrigWS = window.WebSocket
22037
+ function PatchedWS(url, protocols) {
22038
+ try {
22039
+ var u = new URL(url, location.href)
22040
+ if (u.host === location.host) {
22041
+ u.pathname = reroot(u.pathname)
22042
+ u.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
22043
+ url = u.toString()
22044
+ }
22045
+ } catch (e) { /* malformed URL \u2014 let the real constructor throw */ }
22046
+ return protocols === undefined ? new OrigWS(url) : new OrigWS(url, protocols)
22047
+ }
22048
+ PatchedWS.prototype = OrigWS.prototype
22049
+ PatchedWS.CONNECTING = OrigWS.CONNECTING
22050
+ PatchedWS.OPEN = OrigWS.OPEN
22051
+ PatchedWS.CLOSING = OrigWS.CLOSING
22052
+ PatchedWS.CLOSED = OrigWS.CLOSED
22053
+ window.WebSocket = PatchedWS
22054
+ document.addEventListener('click', function (e) {
22055
+ var t = e.target
22056
+ var a = t && t.closest ? t.closest('a[href]') : null
22057
+ if (!a) return
22058
+ var href = a.getAttribute('href')
22059
+ if (href) a.setAttribute('href', reroot(href))
22060
+ }, true)
22061
+ })()</script>`;
22018
22062
  async function proxyCicdAppRequest(db, workspacePath, projectId, prefix, subPath, req) {
22019
22063
  const location = resolveCicdApp(db, workspacePath, projectId);
22020
22064
  if (!location.present) throw svcError6("CICD_APP_MISSING", 404);
@@ -22047,6 +22091,13 @@ async function proxyCicdAppRequest(db, workspacePath, projectId, prefix, subPath
22047
22091
  if (redirectTo?.startsWith("/") && redirectTo !== prefix && !redirectTo.startsWith(`${prefix}/`)) {
22048
22092
  responseHeaders.set("location", `${prefix}${redirectTo}`);
22049
22093
  }
22094
+ const contentType = upstream.headers.get("content-type") ?? "";
22095
+ if (contentType.includes("text/html")) {
22096
+ const html = await upstream.text();
22097
+ const headTag = html.match(/<head[^>]*>/i);
22098
+ const patched = headTag ? html.replace(headTag[0], `${headTag[0]}${PROXY_BASE_SHIM}`) : `${PROXY_BASE_SHIM}${html}`;
22099
+ return new Response(patched, { status: upstream.status, headers: responseHeaders });
22100
+ }
22050
22101
  return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
22051
22102
  }
22052
22103
  var CICD_PROXY_UPGRADE_RE = /^\/api\/cicd\/(\d+)\/app-proxy(\/.*)$/;
@@ -0,0 +1,4 @@
1
+ -- v2.3.1: Persist the generated CI/CD app's guard token. The token was in-memory only, so a
2
+ -- studio restart orphaned a still-running app: its spawn-env token no longer validated against
3
+ -- /api/cicd-guard, the develop gate read "studio unreachable" and every control stayed disabled.
4
+ ALTER TABLE `cicd_setups` ADD `guard_token` text;
@@ -141,6 +141,13 @@
141
141
  "when": 1785000000000,
142
142
  "tag": "0019_git_provider",
143
143
  "breakpoints": true
144
+ },
145
+ {
146
+ "idx": 20,
147
+ "version": "6",
148
+ "when": 1785100000000,
149
+ "tag": "0020_cicd_guard_token",
150
+ "breakpoints": true
144
151
  }
145
152
  ]
146
- }
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rashidee/co2",
3
- "version": "1.3.15",
3
+ "version": "1.3.17",
4
4
  "description": "Compound Context Studio — self-hosted team context authoring for CO2 projects",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,12 +21,12 @@ description: >
21
21
  copy, docker run or kubectl apply, or none). Controls are disabled for any application that
22
22
  has never completed a conductor-develop workflow run (checked against the studio's
23
23
  /api/cicd-guard endpoint using the spawn-env token). Health/port probing marks apps already
24
- running — even ones started outside this app. The top bar carries a version selector (fed by
25
- the studio guard's versions, latest preselected) whose choice replaces the {VERSION} token in
26
- commands docker images are tagged <repo>/<app>:<version> when the plan's Docker section is
27
- active (managed Dockerfiles materialized before Build when missing; Build never pushes) — and
28
- Build All / Start All / Stop All buttons operating every eligible application at once
29
- (sequential builds). When the approved plan declares an `## Infrastructure` list, the
24
+ running — even ones started outside this app. There is NO version selector every control
25
+ operates on the current working tree, and the {VERSION} token in commands auto-resolves to
26
+ the project's current version (studio guard's latest semver, fallback `latest`) docker
27
+ images are tagged <repo>/<app>:<version> when the plan's Docker section is active (managed
28
+ Dockerfiles materialized before Build when missing; Build never pushes) and Build All /
29
+ Start All / Stop All buttons operate every eligible application at once (sequential builds). When the approved plan declares an `## Infrastructure` list, the
30
30
  main page shows an "Infrastructure" link that opens a separate status-only page listing every
31
31
  supporting 3rd-party service with a live TCP-reachability indicator and a console link. Writes cicd/CICD_GENERATION.md with `**Status**:` IN PROGRESS →
32
32
  COMPLETED so the studio watcher detects completion. Idempotent — re-running regenerates
@@ -76,7 +76,8 @@ Read `references/cicd-app-template.md` for the complete file templates. Produce:
76
76
  `## Dockerfiles` section, or `{ "path": "Dockerfile", "content": null }` for an `existing`
77
77
  one (`null` entirely when the app has no image). Copy managed Dockerfile bodies verbatim.
78
78
  Commands may carry the literal `{VERSION}` token — copy it verbatim; the generated app
79
- substitutes it at execution. `deployMethod` is the plan's Deploy Method column
79
+ substitutes it at execution with the project's CURRENT version (studio guard's latest
80
+ semver, `v` prefix stripped, fallback `latest`) — never via any user-facing selector. `deployMethod` is the plan's Deploy Method column
80
81
  (`none|web-server|docker|kubernetes`) and `deploy` its command (`null` when the method is
81
82
  `none`). The `infra` array comes from the plan's `## Infrastructure` table (one entry per
82
83
  row); `url` is `null` when the Console URL cell is `-`. Emit `"infra": []` when the plan's
@@ -111,18 +112,49 @@ port, application count) and STOP — the studio starts/embeds the app.
111
112
  once at boot). Never cache the parsed config in a top-level `const`.
112
113
  - The page is bare: no header, sidebar or footer (it is iframed by the studio). Light theme,
113
114
  one nav tab per application; the tab body is that application's live log.
114
- - **Path-prefix proxy awareness (MANDATORY)**: the studio embeds the page through its
115
- same-origin reverse proxy at `/api/cicd/<projectId>/app-proxy/` — root-absolute client URLs
116
- (`fetch('/api/state')`, `href="/infra"`, `ws://host/ws`) escape the prefix, hit the studio
117
- instead of this app and render an empty page. The client script MUST derive a base path once —
118
- `const BASE = location.pathname.endsWith('/') ? location.pathname.slice(0,-1) : location.pathname`
119
- (on the `/infra` page additionally strip the trailing `/infra`) — and prefix EVERY fetch
120
- (`fetch(BASE+'/api/state')`), the Infrastructure link (`BASE+'/infra'`) and the log WebSocket.
121
- The WebSocket URL must also follow the page scheme:
122
- `(location.protocol==='https:'?'wss://':'ws://')+location.host+BASE+'/ws?app=...'` a
123
- hardcoded `ws://` is blocked as mixed content when the studio is served over HTTPS.
115
+ - **Path-prefix proxy awareness (MANDATORY — copy the template below verbatim)**: the studio
116
+ embeds the page through its same-origin reverse proxy at `/api/cicd/<projectId>/app-proxy/` —
117
+ root-absolute client URLs (`fetch('/api/state')`, `href="/infra"`, `ws://host/ws`) escape the
118
+ prefix, hit the studio instead of this app and render an EMPTY BLACK PAGE in the studio.
119
+ Every generated page's client `<script>` MUST begin with this exact bootstrap block:
120
+
121
+ ```js
122
+ // Proxy-aware base path the studio serves this page at /api/cicd/<id>/app-proxy/ (main)
123
+ // and .../app-proxy/infra; direct access serves / and /infra. All client URLs derive from it.
124
+ // Regex-free ON PURPOSE see the template-literal escaping rule below.
125
+ var _p = location.pathname
126
+ if (_p.endsWith('/infra')) _p = _p.slice(0, -6)
127
+ if (_p.endsWith('/')) _p = _p.slice(0, -1)
128
+ var BASE = _p
129
+ function apiUrl(path) { return BASE + path } // apiUrl('/api/state')
130
+ function wsUrl(path) { // wsUrl('/ws?app=x')
131
+ return (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + BASE + path
132
+ }
133
+ ```
134
+
135
+ and then NEVER write a root-absolute URL anywhere in client code:
136
+ - every fetch goes through `apiUrl(...)` — `fetch(apiUrl('/api/state'))`, never `fetch('/api/state')`;
137
+ - the log stream socket is `new WebSocket(wsUrl('/ws?app='+encodeURIComponent(name)))` —
138
+ never a hardcoded `ws://` (mixed-content-blocked when the studio is served over HTTPS);
139
+ - the Infrastructure link is set from script (`infralink.href = BASE + '/infra'`), never a
140
+ literal `href="/infra"`.
124
141
  Server-side routes stay unprefixed (`/api/state`, `/ws`, `/infra`) — the studio proxy strips
125
142
  the prefix before forwarding.
143
+ - **Template-literal escaping in client code (MANDATORY)**: the page HTML/JS is emitted from a
144
+ template literal inside `server.js`, where every backslash is consumed once when the template
145
+ cooks — a client-side regex like `.replace(/\/$/, '')` reaches the browser as
146
+ `.replace(//$/, '')`, the `//` starts a comment, and the WHOLE client script dies with a
147
+ SyntaxError (blank page both direct AND embedded; this exact regression has shipped before).
148
+ Client code inside the template must therefore contain NO backslashes at all: use string
149
+ methods (`endsWith`, `slice`, `split`, `indexOf`) instead of regex literals, and avoid `\n`
150
+ in client strings (concatenate with real newlines or use `String.fromCharCode(10)`).
151
+ - **Syntax self-check gate (MANDATORY before flipping the generation COMPLETED)**:
152
+ 1. `node --check server.js` (validates the server code);
153
+ 2. start the server briefly, fetch `/` and `/infra`, extract every `<script>…</script>` block
154
+ and run `new Function(script)` on each — any SyntaxError means the page renders blank;
155
+ 3. grep the client sections — any remaining `fetch('/`, `href="/`, `'ws://'` or backslash
156
+ inside the page template is a defect; fix via `apiUrl`/`wsUrl`/`BASE` and the escaping
157
+ rule first. Only then flip `CICD_GENERATION.md` to COMPLETED.
126
158
  - Infrastructure page: when `infra` is non-empty the main page shows an "Infrastructure" link
127
159
  that opens the separate `/infra` page in a new tab — a status-only list of supporting
128
160
  3rd-party services, each with a live TCP-reachability dot (polled) and a console link when a
@@ -132,12 +164,13 @@ port, application count) and STOP — the studio starts/embeds the app.
132
164
  (`<project>/<folder>`) as working directory — command paths are relative to the application
133
165
  folder (e.g. `java -jar target\app.jar`), never to the project root. Deploy renders only
134
166
  when the plan assigned a deploy method other than `none`.
135
- - **Version selector (topbar)**: the page's top bar carries a version dropdown fed by the
136
- studio guard response's `versions` array (already polled every 30 s; latest first —
137
- preselected). When the studio is unreachable the selector holds a single `latest` entry.
138
- The selected semver, minus its `v` prefix, replaces every literal `{VERSION}` in
139
- build/start/stop/deploy commands at execution time (fallback `latest`). Tag-only
140
- semantics: the code built is always the working tree the selector never checks out code.
167
+ - **NO version selector — current codebase only**: the page carries NO version dropdown or
168
+ any other version choice. Build/Start/Stop/Deploy (and the All buttons) always operate on
169
+ the CURRENT working tree — there is nothing else they could build. The literal `{VERSION}`
170
+ token in commands is resolved AUTOMATICALLY at execution time to the project's current
171
+ version: the FIRST entry of the studio guard response's `versions` array (already polled
172
+ every 30 s; latest first), minus its `v` prefix, falling back to `latest` when the studio
173
+ is unreachable. Never render a UI control for it and never check out code.
141
174
  - **Dockerfile materialization**: before Build, when an app's `dockerfile.content` is set and
142
175
  no file exists at its path in the application folder, the server writes it (never
143
176
  overwriting an existing Dockerfile — mirror of the managed-config rule). Build never
@@ -1,310 +1,312 @@
1
- ---
2
- name: util-plancicd
3
- model: claude-opus-4-8
4
- effort: high
5
- description: >
6
- Infer a local-deployment plan for every custom application of a CO2 project and write it to
7
- cicd/CICD_PLAN.md for human review in Compound Context Studio. Reads the project's
8
- ENVIRONMENT.md and DEVTOOL.md, CLAUDE.md (# Custom Applications, # Port Allocation) and each
9
- application's context/specification/SPECIFICATION.md, then decides per application: deployment
10
- method (container vs bare process), build command (e.g. Spring Boot jar vs Docker image),
11
- start command, stop mechanism, deploy method + command (none / web-server copy / docker run /
12
- kubectl apply — only when the environment calls for one), port and health-check URL, plus a
13
- port for the generated CI/CD app itself. When Docker AND a docker repo/registry are both
14
- configured (DEVTOOL.md + ENVIRONMENT.md), it additionally plans the docker-image build path:
15
- per-application image names under the repo, Build commands extended with
16
- `docker build -t <image>:{VERSION}` (the {VERSION} token is resolved by the generated CI/CD
17
- app from its topbar version selector), and a managed Dockerfile authored into the plan's
18
- `## Dockerfiles` section for any dockerized application lacking one (materialized before
19
- Build, never overwriting an existing file). It also infers each application's RUNTIME CONFIG — the
20
- config files its start command reads (e.g. `.env`), deciding per file whether the values come
21
- from ENVIRONMENT.md or from an existing `.env` / `.env-*` file already in the app folder, listing
22
- the required keys and, for ENVIRONMENT.md-sourced files, the resolved content — so the generated
23
- CI/CD app can complete the config before start (guaranteeing the app comes up) and show it in a
24
- per-application Config modal for troubleshooting. It also enumerates the project's supporting
25
- infrastructure (3rd-party services from CLAUDE.md's `# Supporting 3rd Party Applications` and
26
- ENVIRONMENT.md — host, port and optional console URL) into an `## Infrastructure` section so
27
- the generated CI/CD app can surface a status-only Infrastructure page. The plan carries a
28
- top-level
29
- `**Status**: IN PROGRESS` header flipped to `COMPLETED` when done — the studio's watcher reads
30
- it. This skill NEVER generates the CI/CD app (that is util-gencicdscript, which runs only
31
- after the studio stamps an **Approved**: line into the plan). Trigger on keywords: "plan
32
- cicd", "plan deployment", "cicd plan", "infer deployment", "deployment plan", "plan ci/cd".
33
- Accepts one MANDATORY argument: the project-relative path of the plan-request markdown the
34
- studio generated (e.g., /util-plancicd execution/cicd-plan-3-1753000000000.md).
35
- ---
36
-
37
- # Util Plan CI/CD
38
-
39
- ## Inputs
40
-
41
- ```
42
- /util-plancicd <path-to-plan-request-md>
43
- ```
44
-
45
- | Argument | Required | Description |
46
- |---|---|---|
47
- | path | yes | Project-relative path of the studio-generated plan-request markdown under `execution/` |
48
-
49
- If the argument is missing or the file does not exist, STOP and tell the user to click
50
- **Plan deployment** in the studio's CI/CD screen first.
51
-
52
- ### Auto-Resolved Paths
53
-
54
- | Logical file | Path |
55
- |---|---|
56
- | Plan request | `<argument>` |
57
- | Environment | `ENVIRONMENT.md` (project root) |
58
- | Dev tools | `DEVTOOL.md` (project root) |
59
- | Project context | `CLAUDE.md` (project root; `source/CLAUDE.md` fallback) |
60
- | App specification | `<app_folder>/context/specification/SPECIFICATION.md` |
61
- | App config files | `<app_folder>/.env`, `<app_folder>/.env-*` (and stack-conventional config files) |
62
- | Infrastructure | `CLAUDE.md` `# Supporting 3rd Party Applications` + `ENVIRONMENT.md` |
63
- | Output plan | `cicd/CICD_PLAN.md` |
64
-
65
- ## Phase 1 — Gather
66
-
67
- 1. Read the plan-request markdown; its `## Applications` list is the authoritative application set.
68
- 2. Read `ENVIRONMENT.md` (registries, credentials, external services) and `DEVTOOL.md`
69
- (installed tools + paths: node/pnpm/docker/kubectl/python versions).
70
- 2a. **Docker facts**: record whether Docker is installed (DEVTOOL.md) AND whether
71
- ENVIRONMENT.md names a **docker repo/registry** — a registry URL or namespace the images
72
- are tagged for (e.g. `registry.example.com/team`, a Docker Hub org, an ECR/GCR path).
73
- The docker-image build path (below) activates ONLY when BOTH are present; Docker without
74
- a configured repo keeps today's behavior (no image builds).
75
- 3. Read `CLAUDE.md`: `# Custom Applications` (folder names, Depends on), `# Port Allocation`
76
- (per-application ports), and `# Supporting 3rd Party Applications` (the infrastructure the
77
- applications depend on — databases, caches, object storage, SSO, mail, message brokers, etc.).
78
- 4. For each application, read its SPECIFICATION.md tech-stack section if present; otherwise
79
- infer the stack from build files (`package.json` → node, `pom.xml` → spring-boot,
80
- `composer.json` → laravel).
81
- 4a. For each application, discover its RUNTIME CONFIG files: list any existing `.env` / `.env-*`
82
- in the application folder AND the stack-conventional config the start command reads (node/
83
- laravel → `.env`; Spring Boot → `application.properties` / `application-<profile>.yml` or a
84
- `.env` when the app loads one). Read the content of each existing file (values may be reused).
85
- Cross-reference ENVIRONMENT.md for the per-application config values (DB/cache/broker hosts and
86
- ports, external-service URLs, credentials, secrets, the app's own PORT) and the Infrastructure
87
- list from step 5 for the hosts/ports the app must point at.
88
- 5. Build the **infrastructure list**: one entry per supporting 3rd-party service the project
89
- declares. For each, resolve `host` (default `127.0.0.1`), `port` and — when it exposes a web
90
- console/UI — a `url`, cross-referencing ENVIRONMENT.md for the actual host/port/endpoint. A
91
- service with no host:port to probe (or a purely external SaaS with no local endpoint) is
92
- omitted. If the project declares no supporting 3rd-party services, the infrastructure list is
93
- empty and the plan's `## Infrastructure` section is written empty.
94
-
95
- ## Phase 2 — Decide (per application)
96
-
97
- - **Working directory (CRITICAL)**: the generated CI/CD app executes every command with the
98
- **application folder** (`<project>/<folder>`) as its working directory — NEVER the project
99
- root. All paths inside build/start/stop/deploy commands must therefore be relative to the
100
- application folder (`mvn -q package`, `java -jar target\app.jar`) — never prefixed with the
101
- application folder itself (`skolafund_middleware\target\...` is WRONG).
102
- - **Method**: `container` only when Docker appears in DEVTOOL.md AND the stack has a sensible
103
- container path; otherwise `process` (bare local process — the default for a dev VM).
104
- - **Build command**: from the stack (e.g. `pnpm install && pnpm build`, `mvn -q package`,
105
- `composer install`). Use tool paths from DEVTOOL.md when the tool is not on PATH.
106
- - **Start command**: the stack's production-ish local start (e.g. `node dist/index.js`,
107
- `java -jar target/<artifact>.jar`, `php artisan serve --port <port>`); `docker build`/
108
- `docker run` pair when method=container. For jar-based stacks, resolve the EXACT artifact
109
- filename — read the pom/gradle build config: a Maven `<finalName>` means
110
- `target/<finalName>.jar` with NO version suffix (Spring Boot repackage keeps that name and
111
- leaves a `.jar.original` beside it — never reference the `.original`). Never guess
112
- `<app>-<version>.jar` and never use a `target/*.jar` glob (shell-dependent, breaks on
113
- Windows cmd).
114
- - **Stop**: `process-kill` for bare processes; `docker stop <name>` for containers.
115
- - **Deploy** (method + command): only when ENVIRONMENT.md/DEVTOOL.md show a deployment target —
116
- `kubernetes` (`kubectl apply -f <app>/k8s/` when kubectl + a cluster/kubeconfig appear),
117
- `docker` (`docker run`/`docker compose up -d` when the image is the deliverable),
118
- `web-server` (copy the built artifact into the configured web/app server, e.g. nginx html
119
- root or Tomcat webapps, when one is configured). Default **`none`** — a bare local process
120
- needs no deploy step beyond Start; never invent a target the config does not name.
121
- - **Docker image** (only when Gather 2a found Docker + a docker repo — otherwise skip this
122
- bullet entirely):
123
- - **Image name**: `<repo>/<app_folder>` — the application folder name, lowercased, with
124
- any character docker rejects replaced by `-` (keep a leading `<number>_` prefix as-is;
125
- e.g. `registry.example.com/team/skolafund_web`).
126
- - **Build**: append the image build to the app build —
127
- `<app build> && docker build -t <image>:{VERSION} .` The literal `{VERSION}` token is
128
- the contract: the generated CI/CD app substitutes the version selected in its topbar
129
- (semver without the `v` prefix; `latest` fallback) into commands at execution time.
130
- Build never pushes pushing belongs to a `docker` Deploy method.
131
- - **Container start/deploy**: when method=container, reference `<image>:{VERSION}` in the
132
- start/deploy commands too, so the selected tag is what runs.
133
- - **Dockerfile**: if the application folder already has a `Dockerfile`, use it as-is
134
- (`existing`). Otherwise author a stack-appropriate Dockerfile (node / spring-boot /
135
- laravel same stack detection as depgen-k8s) into the plan's `## Dockerfiles` section
136
- (`managed`); the generated CI/CD app materializes it before Build only when the file is
137
- still missing on disk.
138
- - **Port**: from `# Port Allocation`; if absent, assign sequentially from 3000 avoiding
139
- collisions with the studio (3001) and the CI/CD app port.
140
- - **Health / running-detection**: the endpoint the generated CI/CD app uses to tell whether the
141
- application is already running. Detection is a **raw TCP connect to the app's port on loopback**
142
- the generated app probes BOTH IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback, plus the host
143
- named in this Health value so it does not matter which family the framework binds. This
144
- matters because dev servers differ: Spring Boot binds `127.0.0.1`, but **Vite / Next preview
145
- servers bind IPv6 `::1` (localhost) only** and are invisible to an IPv4-only probe. Therefore
146
- set the Health host to **`localhost`** (not `127.0.0.1`) for any Node/Vite/Next dev-server app
147
- so the detection host set includes the interface it actually binds; use `127.0.0.1` for JVM
148
- apps. Value form: `http://localhost:<port>/` or a documented health path from the spec (the
149
- path is informational detection keys on the port, not the path).
150
- - **CI/CD app port**: 5000 unless taken in `# Port Allocation`; record it in the plan.
151
-
152
- ### Application config (per application)
153
-
154
- Decide the runtime config the application needs so it STARTS SUCCESSFULLY, and where each value
155
- comes from. For every config file the start command reads (`path`, relative to the application
156
- folder):
157
-
158
- - **source** — classify each file:
159
- - `env-file`an existing `.env` / `.env-*` in the app folder already holds the runtime values;
160
- use it as-is on disk (do NOT re-materialize it). Set `managed: no`, no resolved content.
161
- - `environment` there is no complete runtime file; resolve the required values from
162
- ENVIRONMENT.md (and the Infrastructure hosts/ports) and record the fully-resolved file content.
163
- Set `managed: yes` so the CI/CD app writes it before Build/Start when missing.
164
- - `derived` the runtime file is chosen/combined from existing material (e.g. copy the target
165
- env's `.env-<env>` to the canonical `.env` the start command reads, or merge an existing
166
- `.env-*` with ENVIRONMENT.md values). Set `managed: yes` and record the resolved content.
167
- - **required keys** enumerate every key the app must have to boot: its own PORT, datastore/cache/
168
- broker connection strings, external-service URLs, credentials/secrets. Cross-check them against
169
- ENVIRONMENT.md and the Infrastructure list.
170
- - **completeness (the point of this)** — every required key MUST be satisfiable from ENVIRONMENT.md
171
- or an existing env file. If a required key cannot be resolved, still emit the config entry but
172
- call out the unresolved key in that application's Rationale as a GAP (so the human fixes
173
- ENVIRONMENT.md before approving) never silently drop it.
174
- - **managed content**for a `managed: yes` file, write the resolved body verbatim into the plan
175
- as a fenced block (`path=<file>`). Values (including credentials) are emitted verbatim under the
176
- dev-environment plaintext rule; they must never appear in the Rationale narrative.
177
-
178
- Only list files the app actually reads at runtime — do not invent config files. An app that needs
179
- no config file gets an empty config list.
180
-
181
- ### Infrastructure (status-only)
182
-
183
- Supporting 3rd-party services are **not** built/started/deployed by the CI/CD app — they are
184
- surfaced status-only. For each service resolved in Gather step 5, decide:
185
-
186
- - **id**: a stable kebab-case key unique within the plan (e.g. `postgres-main`, `keycloak`).
187
- - **name**: display name (e.g. `PostgreSQL (skf_main)`, `Keycloak`).
188
- - **host** / **port**: the address the CI/CD app TCP-probes for reachability; default host
189
- `127.0.0.1`. Use the values ENVIRONMENT.md documents.
190
- - **url**: the web console/UI opened in a new tab, when the service has one (Keycloak admin,
191
- MinIO console, Mailcatcher inbox); `-` for headless services (PostgreSQL, Redis).
192
- - **description**: a short blurb (e.g. `Shared app database`, `S3-compatible object storage`).
193
-
194
- Never emit credentials into the infrastructure entries — status and links only.
195
-
196
- ## Phase 3 — Write the plan
197
-
198
- Create `cicd/` if missing and write `cicd/CICD_PLAN.md` (shown here in a 4-backtick fence so the
199
- inner ```env config block renders the plan file itself uses a normal 3-backtick env fence):
200
-
201
- ````markdown
202
- # CI/CD Deployment Plan — <Project Name>
203
-
204
- **Generated**: <ISO date>
205
- **Status**: IN PROGRESS
206
-
207
- ## CI/CD App
208
-
209
- - **Port**: <port>
210
- - **Folder**: cicd/app
211
-
212
- ## Deployment Decisions
213
-
214
- | Application | Folder | Method | Port | Build | Start | Stop | Deploy Method | Deploy | Health |
215
- |---|---|---|---|---|---|---|---|---|---|
216
- | <name> | <folder> | process|container | <port> | `<cmd>` | `<cmd>` | process-kill|docker stop | none|web-server|docker|kubernetes | `<cmd or ->` | <url> |
217
-
218
- > **Running-detection**: the generated CI/CD app determines "is it running?" by a TCP connect to
219
- > each application's **Port** on both IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback plus the
220
- > **Health** host so an app is detected whichever family it binds. Use a `localhost` Health host
221
- > for Node/Vite/Next apps (they bind IPv6 `::1`) and `127.0.0.1` for JVM apps.
222
-
223
- > **`{VERSION}` token**: Build/Start/Deploy commands may contain the literal `{VERSION}`;
224
- > the generated CI/CD app replaces it with the version selected in its topbar (semver
225
- > without the `v` prefix, `latest` when unknown) at execution time. Used by the docker-image
226
- > path to tag images; tag-only the code built is always the working tree.
227
-
228
- ## Docker
229
-
230
- Written only from Gather 2a. When Docker or a docker repo is absent, write the section as:
231
- `_Not dockerized — Docker and/or a docker repo are not configured in DEVTOOL.md/ENVIRONMENT.md._`
232
-
233
- **Registry**: <repo, e.g. `registry.example.com/team`>
234
-
235
- | Application | Image | Dockerfile |
236
- |---|---|---|
237
- | <name> | `<repo>/<app_folder>` | existing \| managed |
238
-
239
- ## Dockerfiles
240
-
241
- One fenced block per `managed` Dockerfile (omit the section body — `_All Dockerfiles exist._`
242
- — when every dockerized app already has one; `_Not dockerized._` when the Docker section is
243
- inactive). The generated CI/CD app writes each body to `<app_folder>/Dockerfile` before Build
244
- only when the file is missing on disk an existing Dockerfile is never overwritten.
245
-
246
- ```dockerfile path=<app_folder>/Dockerfile
247
- FROM node:22-slim
248
- WORKDIR /app
249
- COPY package*.json ./
250
- RUN npm ci --omit=dev
251
- COPY . .
252
- EXPOSE <port>
253
- CMD ["node", "dist/index.js"]
254
- ```
255
-
256
- ## Application Configs
257
-
258
- The runtime config each application needs to START SUCCESSFULLY. The generated CI/CD app writes any
259
- `managed=yes` file into the application folder before Build/Start when it is missing, and shows
260
- every file (source, status, required keys, live content) in the per-application Config modal for
261
- troubleshooting. One table per application; `Path` is relative to the application folder.
262
-
263
- ### <App Name> (<folder>)
264
-
265
- | Path | Source | Managed | Required Keys |
266
- |---|---|---|---|
267
- | .env | environment | yes | DATABASE_URL, REDIS_HOST, PORT |
268
- | .env-uat | env-file | no | DATABASE_URL, REDIS_HOST |
269
-
270
- For each `Managed=yes` row, follow it with the resolved file body (verbatim — credentials allowed
271
- under the dev-plaintext rule):
272
-
273
- ```env path=.env
274
- DATABASE_URL=postgres://app:app@127.0.0.1:5432/appdb
275
- REDIS_HOST=127.0.0.1
276
- PORT=3000
277
- ```
278
-
279
- Omit the fenced block for `env-file` rows (the file is used as-is on disk). If an application needs
280
- no config file, write `_No runtime config files._` under its heading.
281
-
282
- ## Infrastructure
283
-
284
- Supporting 3rd-party services shown status-only (TCP reachability) on the CI/CD app's
285
- Infrastructure page. Leave the table body empty when the project declares no supporting services.
286
-
287
- | Id | Service | Host | Port | Console URL | Description |
288
- |---|---|---|---|---|---|
289
- | <id> | <name> | <host> | <port> | `<url or ->` | <blurb> |
290
-
291
- ## Rationale
292
-
293
- - <per-application: why this method/commands, citing ENVIRONMENT.md/DEVTOOL.md evidence>
294
- ````
295
-
296
- Then flip `**Status**: IN PROGRESS` to `**Status**: COMPLETED`. The studio watcher advances
297
- only on COMPLETED.
298
-
299
- ## STOP — Hand Back to the Human
300
-
301
- Print a one-paragraph summary and STOP. Never invoke util-gencicdscript or any other skill —
302
- the human reviews and approves the plan in the studio's CI/CD screen first.
303
-
304
- ## Important Rules
305
-
306
- - Never write the CI/CD app; never modify PRD.md/CLAUDE.md/BUG.md.
307
- - Never delete an existing `**Approved**:` line if re-planning over a previous plan — overwrite
308
- the whole file WITHOUT any Approved line (a re-plan always requires fresh approval).
309
- - Credentials from ENVIRONMENT.md may be referenced in commands verbatim (dev-environment
310
- plaintext rule) but never echoed into the Rationale narrative.
1
+ ---
2
+ name: util-plancicd
3
+ model: claude-opus-4-8
4
+ effort: high
5
+ description: >
6
+ Infer a local-deployment plan for every custom application of a CO2 project and write it to
7
+ cicd/CICD_PLAN.md for human review in Compound Context Studio. Reads the project's
8
+ ENVIRONMENT.md and DEVTOOL.md, CLAUDE.md (# Custom Applications, # Port Allocation) and each
9
+ application's context/specification/SPECIFICATION.md, then decides per application: deployment
10
+ method (container vs bare process), build command (e.g. Spring Boot jar vs Docker image),
11
+ start command, stop mechanism, deploy method + command (none / web-server copy / docker run /
12
+ kubectl apply — only when the environment calls for one), port and health-check URL, plus a
13
+ port for the generated CI/CD app itself. When Docker AND a docker repo/registry are both
14
+ configured (DEVTOOL.md + ENVIRONMENT.md), it additionally plans the docker-image build path:
15
+ per-application image names under the repo, Build commands extended with
16
+ `docker build -t <image>:{VERSION}` (the {VERSION} token is auto-resolved by the generated
17
+ CI/CD app to the project's current version), and a managed Dockerfile authored into the plan's
18
+ `## Dockerfiles` section for any dockerized application lacking one (materialized before
19
+ Build, never overwriting an existing file). It also infers each application's RUNTIME CONFIG — the
20
+ config files its start command reads (e.g. `.env`), deciding per file whether the values come
21
+ from ENVIRONMENT.md or from an existing `.env` / `.env-*` file already in the app folder, listing
22
+ the required keys and, for ENVIRONMENT.md-sourced files, the resolved content — so the generated
23
+ CI/CD app can complete the config before start (guaranteeing the app comes up) and show it in a
24
+ per-application Config modal for troubleshooting. It also enumerates the project's supporting
25
+ infrastructure (3rd-party services from CLAUDE.md's `# Supporting 3rd Party Applications` and
26
+ ENVIRONMENT.md — host, port and optional console URL) into an `## Infrastructure` section so
27
+ the generated CI/CD app can surface a status-only Infrastructure page. The plan carries a
28
+ top-level
29
+ `**Status**: IN PROGRESS` header flipped to `COMPLETED` when done — the studio's watcher reads
30
+ it. This skill NEVER generates the CI/CD app (that is util-gencicdscript, which runs only
31
+ after the studio stamps an **Approved**: line into the plan). Trigger on keywords: "plan
32
+ cicd", "plan deployment", "cicd plan", "infer deployment", "deployment plan", "plan ci/cd".
33
+ Accepts one MANDATORY argument: the project-relative path of the plan-request markdown the
34
+ studio generated (e.g., /util-plancicd execution/cicd-plan-3-1753000000000.md).
35
+ ---
36
+
37
+ # Util Plan CI/CD
38
+
39
+ ## Inputs
40
+
41
+ ```
42
+ /util-plancicd <path-to-plan-request-md>
43
+ ```
44
+
45
+ | Argument | Required | Description |
46
+ |---|---|---|
47
+ | path | yes | Project-relative path of the studio-generated plan-request markdown under `execution/` |
48
+
49
+ If the argument is missing or the file does not exist, STOP and tell the user to click
50
+ **Plan deployment** in the studio's CI/CD screen first.
51
+
52
+ ### Auto-Resolved Paths
53
+
54
+ | Logical file | Path |
55
+ |---|---|
56
+ | Plan request | `<argument>` |
57
+ | Environment | `ENVIRONMENT.md` (project root) |
58
+ | Dev tools | `DEVTOOL.md` (project root) |
59
+ | Project context | `CLAUDE.md` (project root; `source/CLAUDE.md` fallback) |
60
+ | App specification | `<app_folder>/context/specification/SPECIFICATION.md` |
61
+ | App config files | `<app_folder>/.env`, `<app_folder>/.env-*` (and stack-conventional config files) |
62
+ | Infrastructure | `CLAUDE.md` `# Supporting 3rd Party Applications` + `ENVIRONMENT.md` |
63
+ | Output plan | `cicd/CICD_PLAN.md` |
64
+
65
+ ## Phase 1 — Gather
66
+
67
+ 1. Read the plan-request markdown; its `## Applications` list is the authoritative application set.
68
+ 2. Read `ENVIRONMENT.md` (registries, credentials, external services) and `DEVTOOL.md`
69
+ (installed tools + paths: node/pnpm/docker/kubectl/python versions).
70
+ 2a. **Docker facts**: record whether Docker is installed (DEVTOOL.md) AND whether
71
+ ENVIRONMENT.md names a **docker repo/registry** — a registry URL or namespace the images
72
+ are tagged for (e.g. `registry.example.com/team`, a Docker Hub org, an ECR/GCR path).
73
+ The docker-image build path (below) activates ONLY when BOTH are present; Docker without
74
+ a configured repo keeps today's behavior (no image builds).
75
+ 3. Read `CLAUDE.md`: `# Custom Applications` (folder names, Depends on), `# Port Allocation`
76
+ (per-application ports), and `# Supporting 3rd Party Applications` (the infrastructure the
77
+ applications depend on — databases, caches, object storage, SSO, mail, message brokers, etc.).
78
+ 4. For each application, read its SPECIFICATION.md tech-stack section if present; otherwise
79
+ infer the stack from build files (`package.json` → node, `pom.xml` → spring-boot,
80
+ `composer.json` → laravel).
81
+ 4a. For each application, discover its RUNTIME CONFIG files: list any existing `.env` / `.env-*`
82
+ in the application folder AND the stack-conventional config the start command reads (node/
83
+ laravel → `.env`; Spring Boot → `application.properties` / `application-<profile>.yml` or a
84
+ `.env` when the app loads one). Read the content of each existing file (values may be reused).
85
+ Cross-reference ENVIRONMENT.md for the per-application config values (DB/cache/broker hosts and
86
+ ports, external-service URLs, credentials, secrets, the app's own PORT) and the Infrastructure
87
+ list from step 5 for the hosts/ports the app must point at.
88
+ 5. Build the **infrastructure list**: one entry per supporting 3rd-party service the project
89
+ declares. For each, resolve `host` (default `127.0.0.1`), `port` and — when it exposes a web
90
+ console/UI — a `url`, cross-referencing ENVIRONMENT.md for the actual host/port/endpoint. A
91
+ service with no host:port to probe (or a purely external SaaS with no local endpoint) is
92
+ omitted. If the project declares no supporting 3rd-party services, the infrastructure list is
93
+ empty and the plan's `## Infrastructure` section is written empty.
94
+
95
+ ## Phase 2 — Decide (per application)
96
+
97
+ - **Working directory (CRITICAL)**: the generated CI/CD app executes every command with the
98
+ **application folder** (`<project>/<folder>`) as its working directory — NEVER the project
99
+ root. All paths inside build/start/stop/deploy commands must therefore be relative to the
100
+ application folder (`mvn -q package`, `java -jar target\app.jar`) — never prefixed with the
101
+ application folder itself (`skolafund_middleware\target\...` is WRONG).
102
+ - **Method**: `container` only when Docker appears in DEVTOOL.md AND the stack has a sensible
103
+ container path; otherwise `process` (bare local process — the default for a dev VM).
104
+ - **Build command**: from the stack (e.g. `pnpm install && pnpm build`, `mvn -q package`,
105
+ `composer install`). Use tool paths from DEVTOOL.md when the tool is not on PATH.
106
+ - **Start command**: the stack's production-ish local start (e.g. `node dist/index.js`,
107
+ `java -jar target/<artifact>.jar`, `php artisan serve --port <port>`); `docker build`/
108
+ `docker run` pair when method=container. For jar-based stacks, resolve the EXACT artifact
109
+ filename — read the pom/gradle build config: a Maven `<finalName>` means
110
+ `target/<finalName>.jar` with NO version suffix (Spring Boot repackage keeps that name and
111
+ leaves a `.jar.original` beside it — never reference the `.original`). Never guess
112
+ `<app>-<version>.jar` and never use a `target/*.jar` glob (shell-dependent, breaks on
113
+ Windows cmd).
114
+ - **Stop**: `process-kill` for bare processes; `docker stop <name>` for containers.
115
+ - **Deploy** (method + command): only when ENVIRONMENT.md/DEVTOOL.md show a deployment target —
116
+ `kubernetes` (`kubectl apply -f <app>/k8s/` when kubectl + a cluster/kubeconfig appear),
117
+ `docker` (`docker run`/`docker compose up -d` when the image is the deliverable),
118
+ `web-server` (copy the built artifact into the configured web/app server, e.g. nginx html
119
+ root or Tomcat webapps, when one is configured). Default **`none`** — a bare local process
120
+ needs no deploy step beyond Start; never invent a target the config does not name.
121
+ - **Docker image** (only when Gather 2a found Docker + a docker repo — otherwise skip this
122
+ bullet entirely):
123
+ - **Image name**: `<repo>/<app_folder>` — the application folder name, lowercased, with
124
+ any character docker rejects replaced by `-` (keep a leading `<number>_` prefix as-is;
125
+ e.g. `registry.example.com/team/skolafund_web`).
126
+ - **Build**: append the image build to the app build —
127
+ `<app build> && docker build -t <image>:{VERSION} .` The literal `{VERSION}` token is
128
+ the contract: the generated CI/CD app substitutes the project's CURRENT version
129
+ (semver without the `v` prefix; `latest` fallback) into commands at execution time
130
+ there is no user-facing version selector; only the current codebase is ever built.
131
+ Build never pushes pushing belongs to a `docker` Deploy method.
132
+ - **Container start/deploy**: when method=container, reference `<image>:{VERSION}` in the
133
+ start/deploy commands too, so the current version's tag is what runs.
134
+ - **Dockerfile**: if the application folder already has a `Dockerfile`, use it as-is
135
+ (`existing`). Otherwise author a stack-appropriate Dockerfile (node / spring-boot /
136
+ laravel same stack detection as depgen-k8s) into the plan's `## Dockerfiles` section
137
+ (`managed`); the generated CI/CD app materializes it before Build only when the file is
138
+ still missing on disk.
139
+ - **Port**: from `# Port Allocation`; if absent, assign sequentially from 3000 avoiding
140
+ collisions with the studio (3001) and the CI/CD app port.
141
+ - **Health / running-detection**: the endpoint the generated CI/CD app uses to tell whether the
142
+ application is already running. Detection is a **raw TCP connect to the app's port on loopback**
143
+ the generated app probes BOTH IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback, plus the host
144
+ named in this Health value so it does not matter which family the framework binds. This
145
+ matters because dev servers differ: Spring Boot binds `127.0.0.1`, but **Vite / Next preview
146
+ servers bind IPv6 `::1` (localhost) only** and are invisible to an IPv4-only probe. Therefore
147
+ set the Health host to **`localhost`** (not `127.0.0.1`) for any Node/Vite/Next dev-server app
148
+ so the detection host set includes the interface it actually binds; use `127.0.0.1` for JVM
149
+ apps. Value form: `http://localhost:<port>/` or a documented health path from the spec (the
150
+ path is informational detection keys on the port, not the path).
151
+ - **CI/CD app port**: 5000 unless taken in `# Port Allocation`; record it in the plan.
152
+
153
+ ### Application config (per application)
154
+
155
+ Decide the runtime config the application needs so it STARTS SUCCESSFULLY, and where each value
156
+ comes from. For every config file the start command reads (`path`, relative to the application
157
+ folder):
158
+
159
+ - **source**classify each file:
160
+ - `env-file` an existing `.env` / `.env-*` in the app folder already holds the runtime values;
161
+ use it as-is on disk (do NOT re-materialize it). Set `managed: no`, no resolved content.
162
+ - `environment` there is no complete runtime file; resolve the required values from
163
+ ENVIRONMENT.md (and the Infrastructure hosts/ports) and record the fully-resolved file content.
164
+ Set `managed: yes` so the CI/CD app writes it before Build/Start when missing.
165
+ - `derived` the runtime file is chosen/combined from existing material (e.g. copy the target
166
+ env's `.env-<env>` to the canonical `.env` the start command reads, or merge an existing
167
+ `.env-*` with ENVIRONMENT.md values). Set `managed: yes` and record the resolved content.
168
+ - **required keys** enumerate every key the app must have to boot: its own PORT, datastore/cache/
169
+ broker connection strings, external-service URLs, credentials/secrets. Cross-check them against
170
+ ENVIRONMENT.md and the Infrastructure list.
171
+ - **completeness (the point of this)** every required key MUST be satisfiable from ENVIRONMENT.md
172
+ or an existing env file. If a required key cannot be resolved, still emit the config entry but
173
+ call out the unresolved key in that application's Rationale as a GAP (so the human fixes
174
+ ENVIRONMENT.md before approving)never silently drop it.
175
+ - **managed content** — for a `managed: yes` file, write the resolved body verbatim into the plan
176
+ as a fenced block (`path=<file>`). Values (including credentials) are emitted verbatim under the
177
+ dev-environment plaintext rule; they must never appear in the Rationale narrative.
178
+
179
+ Only list files the app actually reads at runtime — do not invent config files. An app that needs
180
+ no config file gets an empty config list.
181
+
182
+ ### Infrastructure (status-only)
183
+
184
+ Supporting 3rd-party services are **not** built/started/deployed by the CI/CD app — they are
185
+ surfaced status-only. For each service resolved in Gather step 5, decide:
186
+
187
+ - **id**: a stable kebab-case key unique within the plan (e.g. `postgres-main`, `keycloak`).
188
+ - **name**: display name (e.g. `PostgreSQL (skf_main)`, `Keycloak`).
189
+ - **host** / **port**: the address the CI/CD app TCP-probes for reachability; default host
190
+ `127.0.0.1`. Use the values ENVIRONMENT.md documents.
191
+ - **url**: the web console/UI opened in a new tab, when the service has one (Keycloak admin,
192
+ MinIO console, Mailcatcher inbox); `-` for headless services (PostgreSQL, Redis).
193
+ - **description**: a short blurb (e.g. `Shared app database`, `S3-compatible object storage`).
194
+
195
+ Never emit credentials into the infrastructure entries — status and links only.
196
+
197
+ ## Phase 3 — Write the plan
198
+
199
+ Create `cicd/` if missing and write `cicd/CICD_PLAN.md` (shown here in a 4-backtick fence so the
200
+ inner ```env config block renders — the plan file itself uses a normal 3-backtick env fence):
201
+
202
+ ````markdown
203
+ # CI/CD Deployment Plan — <Project Name>
204
+
205
+ **Generated**: <ISO date>
206
+ **Status**: IN PROGRESS
207
+
208
+ ## CI/CD App
209
+
210
+ - **Port**: <port>
211
+ - **Folder**: cicd/app
212
+
213
+ ## Deployment Decisions
214
+
215
+ | Application | Folder | Method | Port | Build | Start | Stop | Deploy Method | Deploy | Health |
216
+ |---|---|---|---|---|---|---|---|---|---|
217
+ | <name> | <folder> | process|container | <port> | `<cmd>` | `<cmd>` | process-kill|docker stop | none|web-server|docker|kubernetes | `<cmd or ->` | <url> |
218
+
219
+ > **Running-detection**: the generated CI/CD app determines "is it running?" by a TCP connect to
220
+ > each application's **Port** on both IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback plus the
221
+ > **Health** host so an app is detected whichever family it binds. Use a `localhost` Health host
222
+ > for Node/Vite/Next apps (they bind IPv6 `::1`) and `127.0.0.1` for JVM apps.
223
+
224
+ > **`{VERSION}` token**: Build/Start/Deploy commands may contain the literal `{VERSION}`;
225
+ > the generated CI/CD app replaces it with the project's CURRENT version (semver without
226
+ > the `v` prefix, `latest` when unknown) at execution time no user-facing selector.
227
+ > Used by the docker-image path to tag images; tag-only — the code built is always the
228
+ > working tree.
229
+
230
+ ## Docker
231
+
232
+ Written only from Gather 2a. When Docker or a docker repo is absent, write the section as:
233
+ `_Not dockerized — Docker and/or a docker repo are not configured in DEVTOOL.md/ENVIRONMENT.md._`
234
+
235
+ **Registry**: <repo, e.g. `registry.example.com/team`>
236
+
237
+ | Application | Image | Dockerfile |
238
+ |---|---|---|
239
+ | <name> | `<repo>/<app_folder>` | existing \| managed |
240
+
241
+ ## Dockerfiles
242
+
243
+ One fenced block per `managed` Dockerfile (omit the section body `_All Dockerfiles exist._`
244
+ when every dockerized app already has one; `_Not dockerized._` when the Docker section is
245
+ inactive). The generated CI/CD app writes each body to `<app_folder>/Dockerfile` before Build
246
+ only when the file is missing on disk — an existing Dockerfile is never overwritten.
247
+
248
+ ```dockerfile path=<app_folder>/Dockerfile
249
+ FROM node:22-slim
250
+ WORKDIR /app
251
+ COPY package*.json ./
252
+ RUN npm ci --omit=dev
253
+ COPY . .
254
+ EXPOSE <port>
255
+ CMD ["node", "dist/index.js"]
256
+ ```
257
+
258
+ ## Application Configs
259
+
260
+ The runtime config each application needs to START SUCCESSFULLY. The generated CI/CD app writes any
261
+ `managed=yes` file into the application folder before Build/Start when it is missing, and shows
262
+ every file (source, status, required keys, live content) in the per-application Config modal for
263
+ troubleshooting. One table per application; `Path` is relative to the application folder.
264
+
265
+ ### <App Name> (<folder>)
266
+
267
+ | Path | Source | Managed | Required Keys |
268
+ |---|---|---|---|
269
+ | .env | environment | yes | DATABASE_URL, REDIS_HOST, PORT |
270
+ | .env-uat | env-file | no | DATABASE_URL, REDIS_HOST |
271
+
272
+ For each `Managed=yes` row, follow it with the resolved file body (verbatim — credentials allowed
273
+ under the dev-plaintext rule):
274
+
275
+ ```env path=.env
276
+ DATABASE_URL=postgres://app:app@127.0.0.1:5432/appdb
277
+ REDIS_HOST=127.0.0.1
278
+ PORT=3000
279
+ ```
280
+
281
+ Omit the fenced block for `env-file` rows (the file is used as-is on disk). If an application needs
282
+ no config file, write `_No runtime config files._` under its heading.
283
+
284
+ ## Infrastructure
285
+
286
+ Supporting 3rd-party services shown status-only (TCP reachability) on the CI/CD app's
287
+ Infrastructure page. Leave the table body empty when the project declares no supporting services.
288
+
289
+ | Id | Service | Host | Port | Console URL | Description |
290
+ |---|---|---|---|---|---|
291
+ | <id> | <name> | <host> | <port> | `<url or ->` | <blurb> |
292
+
293
+ ## Rationale
294
+
295
+ - <per-application: why this method/commands, citing ENVIRONMENT.md/DEVTOOL.md evidence>
296
+ ````
297
+
298
+ Then flip `**Status**: IN PROGRESS` to `**Status**: COMPLETED`. The studio watcher advances
299
+ only on COMPLETED.
300
+
301
+ ## STOP Hand Back to the Human
302
+
303
+ Print a one-paragraph summary and STOP. Never invoke util-gencicdscript or any other skill —
304
+ the human reviews and approves the plan in the studio's CI/CD screen first.
305
+
306
+ ## Important Rules
307
+
308
+ - Never write the CI/CD app; never modify PRD.md/CLAUDE.md/BUG.md.
309
+ - Never delete an existing `**Approved**:` line if re-planning over a previous plan — overwrite
310
+ the whole file WITHOUT any Approved line (a re-plan always requires fresh approval).
311
+ - Credentials from ENVIRONMENT.md may be referenced in commands verbatim (dev-environment
312
+ plaintext rule) but never echoed into the Rationale narrative.