@rashidee/co2 1.2.6 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/index.js +5302 -3137
  2. package/drizzle/0011_workflow_history_v152.sql +5 -0
  3. package/drizzle/0012_prd_sections_v160.sql +14 -0
  4. package/drizzle/0013_brainstorm_sessions_v170.sql +10 -0
  5. package/drizzle/0014_bug_module_v180.sql +4 -0
  6. package/drizzle/0015_project_config_v190.sql +23 -0
  7. package/drizzle/0016_cicd_v2100.sql +22 -0
  8. package/drizzle/meta/_journal.json +42 -0
  9. package/package.json +1 -1
  10. package/plugin/README.md +2 -0
  11. package/plugin/SKILLS.md +414 -350
  12. package/plugin/skills/conductor-feature-develop/SKILL.md +1 -1
  13. package/plugin/skills/conductor-feature-prepare/SKILL.md +1 -1
  14. package/plugin/skills/depgen-k8s/SKILL.md +1 -1
  15. package/plugin/skills/tracegen-matrix/SKILL.md +1 -1
  16. package/plugin/skills/util-gencicdscript/SKILL.md +91 -0
  17. package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +414 -0
  18. package/plugin/skills/util-plancicd/SKILL.md +130 -0
  19. package/plugin/skills/util-projectinit/SKILL.md +1 -1
  20. package/plugin/skills/util-projectsync/SKILL.md +1 -1
  21. package/plugin/skills/util-updprd/SKILL.md +85 -15
  22. package/plugin/skills/util-usanalyzer/SKILL.md +1 -1
  23. package/plugin/skills/util-ustagger/SKILL.md +1 -1
  24. package/static/assets/{abnfDiagram-VRR7QNED-DvPuwyCV.js → abnfDiagram-VRR7QNED-pIypz8mS.js} +1 -1
  25. package/static/assets/{arc-DRKw7sjs.js → arc-DzTLPg59.js} +1 -1
  26. package/static/assets/{architectureDiagram-ZJ3FMSHR-CaNcxUbC.js → architectureDiagram-ZJ3FMSHR-CuC9-Bft.js} +1 -1
  27. package/static/assets/{blockDiagram-677ZJIJ3-Demd4AOK.js → blockDiagram-677ZJIJ3-DcerPwn1.js} +1 -1
  28. package/static/assets/{c4Diagram-LMCZKHZV-PVKUfc2x.js → c4Diagram-LMCZKHZV-aH--jsPu.js} +1 -1
  29. package/static/assets/channel-2AGSySJb.js +1 -0
  30. package/static/assets/{chunk-2Q5K7J3B-CExf8ww_.js → chunk-2Q5K7J3B-6kmvWwD5.js} +1 -1
  31. package/static/assets/{chunk-32BRIVSS-CFPaoONF.js → chunk-32BRIVSS-Dgk5Y2Hd.js} +1 -1
  32. package/static/assets/{chunk-5VM5RSS4-DE2esM67.js → chunk-5VM5RSS4-DISUWWIg.js} +1 -1
  33. package/static/assets/{chunk-EX3LRPZG-SON_YdDG.js → chunk-EX3LRPZG-CptcVUzS.js} +1 -1
  34. package/static/assets/{chunk-JWPE2WC7-Qxah8LNr.js → chunk-JWPE2WC7-DpOM2Td1.js} +1 -1
  35. package/static/assets/{chunk-MOJQB5TN-BcWGQV3j.js → chunk-MOJQB5TN-D0mE8XQJ.js} +1 -1
  36. package/static/assets/{chunk-RYQCIY6F-DkDj-OR5.js → chunk-RYQCIY6F-6tMpp_wE.js} +1 -1
  37. package/static/assets/{chunk-V7JOEXUC-BlI1Mrah.js → chunk-V7JOEXUC-DlhAClLX.js} +1 -1
  38. package/static/assets/{chunk-VR4S4FIN-Agf__duj.js → chunk-VR4S4FIN-CnRRTJhL.js} +1 -1
  39. package/static/assets/{chunk-XXDRQBXY-B5CfAh85.js → chunk-XXDRQBXY-BjHtDhhp.js} +1 -1
  40. package/static/assets/classDiagram-OUVF2IWQ-DfuKvXel.js +1 -0
  41. package/static/assets/classDiagram-v2-EOCWNBFH-DfuKvXel.js +1 -0
  42. package/static/assets/{cose-bilkent-JH36ORCC-DO0cQkcy.js → cose-bilkent-JH36ORCC-ywkSH5WV.js} +1 -1
  43. package/static/assets/{cynefin-VYW2F7L2-CfOnsnXc.js → cynefin-VYW2F7L2-rNCSrA_M.js} +1 -1
  44. package/static/assets/{cynefinDiagram-TSTJHNR4-BJ8tULAv.js → cynefinDiagram-TSTJHNR4-CTO-xyj0.js} +1 -1
  45. package/static/assets/{dagre-VKFMJZFB-DfjwzHqP.js → dagre-VKFMJZFB-DXWjc-x2.js} +1 -1
  46. package/static/assets/{diagram-FQU43EPY-CndSOIIM.js → diagram-FQU43EPY-B6Yg08kE.js} +1 -1
  47. package/static/assets/{diagram-G47NLZAW-Czf5hN0Z.js → diagram-G47NLZAW-FdzgI0QA.js} +1 -1
  48. package/static/assets/{diagram-NH7WQ7WH-COAyojGh.js → diagram-NH7WQ7WH-C7MRGBLI.js} +1 -1
  49. package/static/assets/{diagram-OA4YK3LP-H8PoiN2m.js → diagram-OA4YK3LP-xpJMJx9r.js} +1 -1
  50. package/static/assets/{diagram-WEI45ONY-DzcaVzjp.js → diagram-WEI45ONY-D5PmiZy-.js} +1 -1
  51. package/static/assets/{ebnfDiagram-CCIWWBDH-DPazGYLJ.js → ebnfDiagram-CCIWWBDH-DJzkD6Lj.js} +1 -1
  52. package/static/assets/{erDiagram-Q63AITRT-Bo-fubgF.js → erDiagram-Q63AITRT-D24quzu9.js} +1 -1
  53. package/static/assets/{flowDiagram-23GEKE2U-CSvxUDwh.js → flowDiagram-23GEKE2U-2wLf_cAi.js} +1 -1
  54. package/static/assets/{ganttDiagram-NO4QXBWP-DnBveWX6.js → ganttDiagram-NO4QXBWP-hjpg1Ww8.js} +1 -1
  55. package/static/assets/{gitGraphDiagram-IHSO6WYX-B3rVL8c-.js → gitGraphDiagram-IHSO6WYX-Zm2bxgq8.js} +1 -1
  56. package/static/assets/{index-y7XDSgMG.js → index-CMSll7Xn.js} +173 -172
  57. package/static/assets/{index-LZSQkLE2.css → index-Dn_JY-18.css} +1 -1
  58. package/static/assets/{infoDiagram-FWYZ7A6U-CvWV9yy2.js → infoDiagram-FWYZ7A6U-5eq5DfJX.js} +1 -1
  59. package/static/assets/{ishikawaDiagram-FXEZZL3T-DF28a1N6.js → ishikawaDiagram-FXEZZL3T-BqBn7GDQ.js} +1 -1
  60. package/static/assets/{journeyDiagram-5HDEW3XC-CB6CugYk.js → journeyDiagram-5HDEW3XC-C6AceyMG.js} +1 -1
  61. package/static/assets/{kanban-definition-HUTT4EX6-ResSLF6d.js → kanban-definition-HUTT4EX6-BeQ_o04F.js} +1 -1
  62. package/static/assets/{linear-CLO--ryk.js → linear-CX49V_yy.js} +1 -1
  63. package/static/assets/{mindmap-definition-LN4V7U3C-BvW3EbxX.js → mindmap-definition-LN4V7U3C-CVzByupH.js} +1 -1
  64. package/static/assets/{pegDiagram-2B236MQR-BHCgqrrM.js → pegDiagram-2B236MQR-GuHmU0j3.js} +1 -1
  65. package/static/assets/{pieDiagram-ENE6RG2P-DVmCQ5FA.js → pieDiagram-ENE6RG2P-BKOiteQS.js} +1 -1
  66. package/static/assets/{quadrantDiagram-ABIIQ3AL-B3e5KR52.js → quadrantDiagram-ABIIQ3AL-CNtd5BPA.js} +1 -1
  67. package/static/assets/{railroadDiagram-RFXS5EU6-Csv5ysLc.js → railroadDiagram-RFXS5EU6-D4gIQQiY.js} +1 -1
  68. package/static/assets/{requirementDiagram-TGXJPOKE-DOMVzvnB.js → requirementDiagram-TGXJPOKE-BYokGs63.js} +1 -1
  69. package/static/assets/{sankeyDiagram-HTMAVEWB-B1dcTOvy.js → sankeyDiagram-HTMAVEWB-5RLXfMzy.js} +1 -1
  70. package/static/assets/{sequenceDiagram-DBY2YBRQ-BY414yeO.js → sequenceDiagram-DBY2YBRQ-DugFTncH.js} +1 -1
  71. package/static/assets/{sizeCapture-X5ZJPWSS-CuJfrfBN.js → sizeCapture-X5ZJPWSS-cCLHi08w.js} +1 -1
  72. package/static/assets/{stateDiagram-2N3HPSRC-FX4rJkzV.js → stateDiagram-2N3HPSRC-DQnxouNu.js} +1 -1
  73. package/static/assets/stateDiagram-v2-6OUMAXLB-BsTf558I.js +1 -0
  74. package/static/assets/{swimlanes-5IMT3BWC-Sfy58mmv.js → swimlanes-5IMT3BWC-CET7P2RH.js} +2 -2
  75. package/static/assets/swimlanesDiagram-G3AALYLV-Crsci1-o.js +8 -0
  76. package/static/assets/{timeline-definition-FHXFAJF6-CmV7by8p.js → timeline-definition-FHXFAJF6-49T4daxW.js} +1 -1
  77. package/static/assets/{vennDiagram-L72KCM5P-FqIyFZFt.js → vennDiagram-L72KCM5P-FqAF-S_r.js} +1 -1
  78. package/static/assets/{wardleyDiagram-EHGQE667-BZ06O45i.js → wardleyDiagram-EHGQE667-C9Du1lj9.js} +1 -1
  79. package/static/assets/{xychartDiagram-FW5EYKEG-BOkffR06.js → xychartDiagram-FW5EYKEG-CY-hJxel.js} +1 -1
  80. package/static/index.html +2 -2
  81. package/plugin/skills/util-preparek8senv/SKILL.md +0 -422
  82. package/static/assets/channel-C2nVbuwv.js +0 -1
  83. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +0 -1
  84. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +0 -1
  85. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +0 -1
  86. package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +0 -8
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conductor-feature-develop
3
- model: claude-sonnet-4-6
3
+ model: claude-sonnet-5
4
4
  effort: high
5
5
  description: >
6
6
  Application development orchestrator — orchestrates full-stack code implementation
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conductor-feature-prepare
3
- model: claude-sonnet-4-6
3
+ model: claude-sonnet-5
4
4
  effort: high
5
5
  description: >
6
6
  Context artifacts preparation orchestrator — generates all context artifacts (module models,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: depgen-k8s
3
- model: claude-sonnet-4-6
3
+ model: claude-sonnet-5
4
4
  effort: high
5
5
  description: >
6
6
  Generate a Dockerfile and Kubernetes manifests for an application targeting a single
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: tracegen-matrix
3
- model: claude-sonnet-4-6
3
+ model: claude-sonnet-5
4
4
  effort: medium
5
5
  description: >
6
6
  Requirement-to-code traceability generator — links every PRD requirement ID (User Stories,
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: util-gencicdscript
3
+ model: claude-sonnet-5
4
+ effort: high
5
+ description: >
6
+ Generate the self-contained CI/CD build/run/deploy app for a CO2 project from an APPROVED
7
+ cicd/CICD_PLAN.md. Refuses to run unless the plan has `**Status**: COMPLETED` AND an
8
+ `**Approved**:` line (stamped by Compound Context Studio's Approve button). Produces
9
+ cicd/app/ — a zero-npm-dependency node:http app (server.js + cicd.config.json) rendering a
10
+ bare light-themed page (no header/sidebar/footer, iframe-embeddable) with one nav tab per
11
+ application: a controls bar (status badge, clickable app URL opening a new tab, Build /
12
+ Start / Stop / Deploy buttons) above that application's log streamed live over a built-in
13
+ zero-dependency WebSocket (bounded tail + backpressure drop server-side, trimmed DOM buffer
14
+ client-side — no leaks/OOM). Build and Deploy commands come from the approved plan (inferred
15
+ from ENVIRONMENT.md/DEVTOOL.md — e.g. Spring Boot jar vs Docker image; deploy via web-server
16
+ copy, docker run or kubectl apply, or none). Controls are disabled for any application that
17
+ has never completed a conductor-develop workflow run (checked against the studio's
18
+ /api/cicd-guard endpoint using the spawn-env token). Health/port probing marks apps already
19
+ running — even ones started outside this app. No version selector: only the latest built
20
+ applications are operated. Writes cicd/CICD_GENERATION.md with `**Status**:` IN PROGRESS →
21
+ COMPLETED so the studio watcher detects completion. Idempotent — re-running regenerates
22
+ cicd/app in place. Trigger on keywords: "generate cicd", "gen cicd script", "generate ci/cd
23
+ app", "cicd app", "generate deployment app". Accepts one MANDATORY argument: the
24
+ project-relative plan path (e.g., /util-gencicdscript cicd/CICD_PLAN.md).
25
+ ---
26
+
27
+ # Util Generate CI/CD Script
28
+
29
+ ## Inputs
30
+
31
+ ```
32
+ /util-gencicdscript cicd/CICD_PLAN.md
33
+ ```
34
+
35
+ ## Phase 0 — Gate (hard requirements, check in order)
36
+
37
+ 1. The argument file exists — else STOP: "run /util-plancicd first".
38
+ 2. It contains a top-level `**Status**: COMPLETED` — else STOP: "the plan is not complete".
39
+ 3. It contains an `**Approved**:` line — else STOP: "approve the plan in the studio's CI/CD
40
+ screen first". NEVER proceed without approval.
41
+
42
+ ## Phase 1 — Write the generation tracking file
43
+
44
+ Write `cicd/CICD_GENERATION.md`:
45
+
46
+ ```markdown
47
+ # CI/CD App Generation
48
+
49
+ **Started**: <ISO date>
50
+ **Status**: IN PROGRESS
51
+ ```
52
+
53
+ ## Phase 2 — Generate cicd/app/
54
+
55
+ Read `references/cicd-app-template.md` for the complete file templates. Produce:
56
+
57
+ 1. `cicd/app/cicd.config.json` — from the plan's tables:
58
+ `{ "port": <CI/CD app port>, "apps": [{ "name", "folder", "method", "port", "build",
59
+ "start", "stop", "deployMethod", "deploy", "health" }] }` — `deployMethod` is the plan's
60
+ Deploy Method column (`none|web-server|docker|kubernetes`) and `deploy` its command
61
+ (`null` when the method is `none`).
62
+ 2. `cicd/app/server.js` — the template verbatim (it is generic; all project specifics live in
63
+ cicd.config.json). Zero npm dependencies.
64
+ 3. Ensure `cicd/logs/` and `cicd/state.json` are ignored: append `cicd/logs/` and
65
+ `cicd/state.json` to the project `.gitignore` if absent.
66
+
67
+ ## Phase 3 — Complete
68
+
69
+ Flip `cicd/CICD_GENERATION.md` `**Status**:` to `COMPLETED`. Print a summary (app folder,
70
+ port, application count) and STOP — the studio starts/embeds the app.
71
+
72
+ ## Important Rules
73
+
74
+ - Zero npm dependencies — plain node:http only (the WebSocket log stream is hand-rolled in the
75
+ template); no install step may be required.
76
+ - The page is bare: no header, sidebar or footer (it is iframed by the studio). Light theme,
77
+ one nav tab per application; the tab body is that application's live log.
78
+ - Build/Start/Stop/Deploy run the plan's commands verbatim, each with the APPLICATION FOLDER
79
+ (`<project>/<folder>`) as working directory — command paths are relative to the application
80
+ folder (e.g. `java -jar target\app.jar`), never to the project root. Deploy renders only
81
+ when the plan assigned a deploy method other than `none`. No version selector — only the
82
+ latest built applications are operated.
83
+ - Develop gate: controls stay disabled for an application that never completed a
84
+ conductor-develop run — checked via `${CO2_STUDIO_URL}/api/cicd-guard/${CO2_PROJECT_ID}`
85
+ with the CO2_GUARD_TOKEN from the spawn env (refreshed every 30 s, last answer cached).
86
+ - Log streaming must stay bounded: per-app log file on disk, bounded tail on connect, live
87
+ append broadcast only, slow subscribers dropped on backpressure, client DOM buffer trimmed.
88
+ - Already-running detection: `/api/state` probes each application's health URL (falling back
89
+ to its port) so an app started outside this page still shows as running; a Stop on an
90
+ externally started process is refused with a hint instead of killed.
91
+ - Never touch PRD.md/CLAUDE.md; never run builds or deploys yourself during generation.
@@ -0,0 +1,414 @@
1
+ # CI/CD App Template
2
+
3
+ `cicd/app/server.js` — generic; reads `cicd.config.json`. Copy verbatim.
4
+
5
+ ```js
6
+ // Generated by util-gencicdscript — zero-dependency CI/CD build/run/deploy app.
7
+ // Env (set by Compound Context Studio at spawn): PORT, CO2_STUDIO_URL, CO2_PROJECT_ID,
8
+ // CO2_GUARD_TOKEN. Bare page — designed to be embedded in the studio as an iframe.
9
+ // One nav tab per application; the tab body streams that application's log live over a
10
+ // hand-rolled WebSocket (server→client push only). Memory safety: logs live on disk, the
11
+ // server retains nothing per subscriber beyond the socket itself (bounded initial tail,
12
+ // slow clients are dropped on backpressure), and the client trims its DOM log buffer.
13
+ const http = require('node:http')
14
+ const { spawn, execFileSync } = require('node:child_process')
15
+ const { createHash } = require('node:crypto')
16
+ const { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, openSync, readSync, closeSync, createWriteStream } = require('node:fs')
17
+ const { join, resolve } = require('node:path')
18
+
19
+ const APP_DIR = __dirname
20
+ const PROJECT_DIR = resolve(APP_DIR, '..', '..')
21
+ const LOG_DIR = join(PROJECT_DIR, 'cicd', 'logs')
22
+ const STATE_FILE = join(PROJECT_DIR, 'cicd', 'state.json')
23
+ const CONFIG = JSON.parse(readFileSync(join(APP_DIR, 'cicd.config.json'), 'utf8'))
24
+ const PORT = Number(process.env.PORT || CONFIG.port || 4100)
25
+ const STUDIO = process.env.CO2_STUDIO_URL || 'http://127.0.0.1:3001'
26
+ const PROJECT_ID = process.env.CO2_PROJECT_ID || ''
27
+ const TOKEN = process.env.CO2_GUARD_TOKEN || ''
28
+
29
+ mkdirSync(LOG_DIR, { recursive: true })
30
+ const running = new Map() // app name -> ChildProcess (only processes THIS app spawned)
31
+ const activity = new Map() // app name -> 'building' | 'deploying' (transient, exclusive)
32
+ const failed = new Map() // app name -> true after a nonzero run exit
33
+ const stopping = new Set() // app names whose exit was requested — not a failure
34
+ const lastError = new Map() // app name -> string
35
+
36
+ function saveState() {
37
+ const s = {}
38
+ for (const a of CONFIG.apps) s[a.name] = running.has(a.name) ? 'running' : 'stopped'
39
+ try { writeFileSync(STATE_FILE, JSON.stringify(s)) } catch {}
40
+ }
41
+
42
+ function currentBranch() {
43
+ try {
44
+ return execFileSync('git', ['branch', '--show-current'], { cwd: PROJECT_DIR }).toString().trim()
45
+ } catch { return null }
46
+ }
47
+
48
+ // ---- Develop gate (Compound Context Studio) --------------------------------------------
49
+ // An application that never completed a conductor-develop run has nothing to build or run —
50
+ // its tab controls are disabled. Refreshed every 30 s; the last known answer is kept when the
51
+ // studio is briefly unreachable, and `null` (never fetched) disables controls with a hint.
52
+ let guardApps = null // Map lower(name|folder) -> developedOnce
53
+ async function refreshGuard() {
54
+ if (!PROJECT_ID) return
55
+ try {
56
+ const r = await fetch(`${STUDIO}/api/cicd-guard/${PROJECT_ID}?token=${TOKEN}`, { signal: AbortSignal.timeout(3000) })
57
+ if (!r.ok) return
58
+ const g = await r.json()
59
+ if (Array.isArray(g.applications)) {
60
+ guardApps = new Map()
61
+ for (const a of g.applications) guardApps.set(String(a.name).toLowerCase(), !!a.developedOnce)
62
+ }
63
+ } catch {}
64
+ }
65
+ function developedOnce(app) {
66
+ if (!guardApps) return null // unknown — controls stay disabled until the studio answers
67
+ const byName = guardApps.get(app.name.toLowerCase())
68
+ if (byName !== undefined) return byName
69
+ const byFolder = guardApps.get(String(app.folder || '').toLowerCase())
70
+ return byFolder === undefined ? null : byFolder
71
+ }
72
+ refreshGuard()
73
+ setInterval(refreshGuard, 30_000).unref?.()
74
+
75
+ // ---- Log streaming ---------------------------------------------------------------------
76
+ // One log file per application (build + run + deploy share it — the tab is one stream).
77
+ // Subscribers get a bounded tail on connect, then live appends; nothing is buffered
78
+ // server-side, and a subscriber that cannot keep up is dropped instead of queuing forever.
79
+ const subscribers = new Map() // app name -> Set<net.Socket>
80
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
81
+
82
+ function wsFrame(data) {
83
+ const payload = Buffer.from(data)
84
+ const len = payload.length
85
+ let header
86
+ if (len < 126) header = Buffer.from([0x81, len])
87
+ else if (len < 65_536) { header = Buffer.alloc(4); header[0] = 0x81; header[1] = 126; header.writeUInt16BE(len, 2) }
88
+ else { header = Buffer.alloc(10); header[0] = 0x81; header[1] = 127; header.writeBigUInt64BE(BigInt(len), 2) }
89
+ return Buffer.concat([header, payload])
90
+ }
91
+
92
+ function broadcast(name, chunk) {
93
+ const subs = subscribers.get(name)
94
+ if (!subs || subs.size === 0) return
95
+ const frame = wsFrame(chunk)
96
+ for (const s of subs) {
97
+ if (s.writableLength > 1_000_000) { subs.delete(s); try { s.destroy() } catch {} ; continue }
98
+ s.write(frame)
99
+ }
100
+ }
101
+
102
+ function logPath(name) { return join(LOG_DIR, `${name}.log`) }
103
+
104
+ function logTail(name, bytes = 16_384) {
105
+ try {
106
+ const p = logPath(name)
107
+ const size = statSync(p).size
108
+ const start = Math.max(0, size - bytes)
109
+ const buf = Buffer.alloc(size - start)
110
+ const fd = openSync(p, 'r')
111
+ readSync(fd, buf, 0, buf.length, start)
112
+ closeSync(fd)
113
+ return buf.toString('utf8')
114
+ } catch { return '' }
115
+ }
116
+
117
+ function logLine(app, text) {
118
+ const line = `\n--- ${new Date().toISOString()} ${text}\n`
119
+ try { createWriteStream(logPath(app.name), { flags: 'a' }).end(line) } catch {}
120
+ broadcast(app.name, line)
121
+ }
122
+
123
+ function runLogged(app, command, onExit) {
124
+ const log = createWriteStream(logPath(app.name), { flags: 'a' })
125
+ log.write(`\n--- ${new Date().toISOString()} $ ${command}\n`)
126
+ broadcast(app.name, `\n--- ${new Date().toISOString()} $ ${command}\n`)
127
+ const child = spawn(command, { cwd: join(PROJECT_DIR, app.folder), shell: true, env: process.env })
128
+ const tap = (d) => { log.write(d); broadcast(app.name, d) }
129
+ child.stdout.on('data', tap)
130
+ child.stderr.on('data', tap)
131
+ child.on('exit', (code) => { log.end(); onExit(code) })
132
+ return child
133
+ }
134
+
135
+ // ---- App lifecycle ---------------------------------------------------------------------
136
+ async function probe(app) {
137
+ const url = app.health || `http://127.0.0.1:${app.port}/`
138
+ try {
139
+ await fetch(url, { signal: AbortSignal.timeout(800) })
140
+ return true
141
+ } catch { return false }
142
+ }
143
+
144
+ function buildApp(app) {
145
+ if (activity.has(app.name)) return
146
+ activity.set(app.name, 'building'); lastError.delete(app.name)
147
+ runLogged(app, app.build, (code) => {
148
+ activity.delete(app.name)
149
+ if (code !== 0) lastError.set(app.name, `build exited ${code} — see log`)
150
+ else logLine(app, 'build OK')
151
+ })
152
+ }
153
+
154
+ function deployApp(app) {
155
+ if (activity.has(app.name) || !app.deploy) return
156
+ activity.set(app.name, 'deploying'); lastError.delete(app.name)
157
+ runLogged(app, app.deploy, (code) => {
158
+ activity.delete(app.name)
159
+ if (code !== 0) lastError.set(app.name, `deploy exited ${code} — see log`)
160
+ else logLine(app, `deploy OK (${app.deployMethod})`)
161
+ })
162
+ }
163
+
164
+ function startApp(app) {
165
+ if (running.get(app.name) || activity.has(app.name)) return
166
+ failed.delete(app.name); lastError.delete(app.name)
167
+ if (app.method === 'container') {
168
+ runLogged(app, app.start, () => {})
169
+ saveState(); return
170
+ }
171
+ const child = runLogged(app, app.start, (code) => {
172
+ running.delete(app.name)
173
+ // A user-requested Stop exits nonzero (killed) — that is not a failure.
174
+ if (code !== 0 && !stopping.delete(app.name)) {
175
+ failed.set(app.name, true)
176
+ lastError.set(app.name, `process exited ${code} — see log`)
177
+ } else stopping.delete(app.name)
178
+ saveState()
179
+ })
180
+ running.set(app.name, child)
181
+ saveState()
182
+ }
183
+
184
+ // shell:true spawns a shell whose grandchildren survive child.kill() on Windows — kill the
185
+ // whole tree there (taskkill /T); plain kill elsewhere.
186
+ function killTree(child) {
187
+ if (process.platform === 'win32' && child.pid) {
188
+ try { execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' }) } catch {}
189
+ } else {
190
+ try { child.kill() } catch {}
191
+ }
192
+ }
193
+
194
+ function stopApp(app) {
195
+ if (app.method === 'container' && app.stop && app.stop !== 'process-kill') {
196
+ try { execFileSync(app.stop.split(' ')[0], app.stop.split(' ').slice(1), { cwd: PROJECT_DIR }) } catch {}
197
+ } else {
198
+ const child = running.get(app.name)
199
+ if (!child) {
200
+ lastError.set(app.name, 'running outside this CI/CD app — stop it from where it was started')
201
+ return
202
+ }
203
+ stopping.add(app.name)
204
+ killTree(child)
205
+ }
206
+ running.delete(app.name)
207
+ saveState()
208
+ }
209
+
210
+ function json(res, code, body) {
211
+ res.writeHead(code, { 'content-type': 'application/json' })
212
+ res.end(JSON.stringify(body))
213
+ }
214
+
215
+ // ---- Page ------------------------------------------------------------------------------
216
+ const PAGE = `<!doctype html><html><head><meta charset="utf-8"><title>CI/CD</title><style>
217
+ body{font-family:system-ui,sans-serif;margin:0;background:#f6f7f9;color:#1c2128;display:flex;flex-direction:column;height:100vh}
218
+ .top{display:flex;gap:8px;align-items:center;padding:10px 12px 6px;font-size:13px}
219
+ .meta{font-size:11px;color:#5b6572}
220
+ .tabs{display:flex;gap:2px;padding:0 12px;border-bottom:1px solid #d9dee5;flex-wrap:wrap}
221
+ .tab{border:1px solid #d9dee5;border-bottom:none;border-radius:8px 8px 0 0;background:#eef1f5;padding:6px 14px;font-size:13px;cursor:pointer;display:flex;gap:6px;align-items:center}
222
+ .tab.active{background:#fff;font-weight:600}
223
+ .tab .dot{width:8px;height:8px;border-radius:99px;background:#8a94a0}
224
+ .dot.running{background:#1c8a4a}.dot.failed{background:#c23b3b}.dot.building,.dot.deploying{background:#c9922a}
225
+ .bar{display:flex;gap:8px;align-items:center;padding:10px 12px;background:#fff;border-bottom:1px solid #e1e5ea;flex-wrap:wrap}
226
+ .badge{font-size:10px;text-transform:uppercase;border-radius:99px;padding:2px 8px;color:#fff;background:#8a94a0}
227
+ .badge.running{background:#1c8a4a}.badge.failed{background:#c23b3b}.badge.building,.badge.deploying{background:#c9922a}
228
+ .url{font-size:12px;color:#1a66d0;text-decoration:underline;word-break:break-all}
229
+ button{background:#fff;color:#1c2128;border:1px solid #c6cdd6;border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer}
230
+ button:hover:enabled{background:#eef1f5}
231
+ button:disabled{opacity:.4;cursor:default}
232
+ .notice{padding:4px 12px;font-size:11px;color:#8a5a00;background:#fff7e6;display:none}
233
+ .err{padding:4px 12px;font-size:11px;color:#c23b3b;display:none;white-space:pre-wrap}
234
+ #log{flex:1;margin:0;padding:10px 12px;background:#0f1216;color:#d7dde4;font-size:11px;line-height:1.5;overflow:auto;white-space:pre-wrap}
235
+ </style></head><body>
236
+ <div class="top"><strong>CI/CD</strong><span id="branch" class="meta"></span></div>
237
+ <div class="tabs" id="tabs"></div>
238
+ <div class="bar" id="bar"></div>
239
+ <div class="notice" id="notice"></div>
240
+ <div class="err" id="err"></div>
241
+ <pre id="log"></pre>
242
+ <script>
243
+ let state = { apps: [] }
244
+ let active = null
245
+ let ws = null, wsTimer = null
246
+
247
+ function appOf(name){ return state.apps.find(a => a.name === name) }
248
+
249
+ function renderTabs(){
250
+ document.getElementById('tabs').innerHTML = state.apps.map(a => \`
251
+ <div class="tab \${a.name===active?'active':''}" onclick="setActive('\${a.name}')">
252
+ <span class="dot \${a.phase}"></span>\${a.name}\${a.developedOnce===false?' 🔒':''}</div>\`).join('')
253
+ }
254
+
255
+ function renderBar(){
256
+ const a = appOf(active)
257
+ if (!a) return
258
+ const url = 'http://'+location.hostname+':'+a.port+'/'
259
+ const gate = a.developedOnce === true
260
+ const busy = a.phase==='building'||a.phase==='deploying'
261
+ document.getElementById('bar').innerHTML = \`
262
+ <span class="badge \${a.phase}">\${a.phase}\${a.external?' (external)':''}</span>
263
+ <span class="meta">\${a.method} · port \${a.port}</span>
264
+ <a class="url" href="\${url}" target="_blank" rel="noreferrer">\${url}</a>
265
+ <span style="flex:1"></span>
266
+ <button onclick="act('build')" \${!gate||busy||a.phase==='running'?'disabled':''}>Build</button>
267
+ <button onclick="act('start')" \${!gate||busy||a.phase==='running'?'disabled':''}>Start</button>
268
+ <button onclick="act('stop')" \${a.phase!=='running'||(a.external&&a.method!=='container')?'disabled':''}>Stop</button>
269
+ \${a.deploy?\`<button onclick="act('deploy')" \${!gate||busy?'disabled':''}>Deploy (\${a.deployMethod})</button>\`:''}\`
270
+ const notice = document.getElementById('notice')
271
+ if (a.developedOnce === false) {
272
+ notice.textContent = 'This application has not been developed through the workflow yet — run conductor-feature-develop first.'
273
+ notice.style.display = 'block'
274
+ } else if (a.developedOnce === null) {
275
+ notice.textContent = 'Develop status not verified yet (studio unreachable) — controls stay disabled.'
276
+ notice.style.display = 'block'
277
+ } else notice.style.display = 'none'
278
+ const err = document.getElementById('err')
279
+ if (a.error) { err.textContent = a.error; err.style.display = 'block' } else err.style.display = 'none'
280
+ }
281
+
282
+ function setActive(name){
283
+ if (active === name) return
284
+ active = name
285
+ closeWs()
286
+ document.getElementById('log').textContent = ''
287
+ openWs(name)
288
+ renderTabs(); renderBar()
289
+ }
290
+
291
+ // Live log stream. One socket at a time (closed on tab switch); auto-reconnect while the tab
292
+ // stays active; the DOM buffer is trimmed so long sessions never grow the page unbounded.
293
+ function openWs(name){
294
+ ws = new WebSocket('ws://'+location.host+'/ws?app='+encodeURIComponent(name))
295
+ ws.onmessage = e => appendLog(e.data)
296
+ ws.onclose = () => { ws = null; if (active === name) wsTimer = setTimeout(() => openWs(name), 2000) }
297
+ }
298
+ function closeWs(){
299
+ if (wsTimer) { clearTimeout(wsTimer); wsTimer = null }
300
+ if (ws) { ws.onclose = null; try { ws.close() } catch {} ws = null }
301
+ }
302
+ function appendLog(text){
303
+ const el = document.getElementById('log')
304
+ const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 40
305
+ el.textContent += text
306
+ if (el.textContent.length > 400000) el.textContent = el.textContent.slice(-250000)
307
+ if (atBottom) el.scrollTop = el.scrollHeight
308
+ }
309
+
310
+ async function refresh(){
311
+ const s = await fetch('/api/state').then(r => r.json()).catch(() => null)
312
+ if (!s) return
313
+ state = s
314
+ document.getElementById('branch').textContent = s.currentBranch ? 'branch: '+s.currentBranch : ''
315
+ if (!active && s.apps.length) setActive(s.apps[0].name)
316
+ renderTabs(); renderBar()
317
+ }
318
+ async function act(op){
319
+ await fetch('/api/apps/'+encodeURIComponent(active)+'/'+op, { method: 'POST' })
320
+ refresh()
321
+ }
322
+ refresh(); setInterval(refresh, 3000)
323
+ </script></body></html>`
324
+
325
+ // ---- HTTP ------------------------------------------------------------------------------
326
+ const server = http.createServer(async (req, res) => {
327
+ if (req.method === 'GET' && req.url === '/') {
328
+ res.writeHead(200, { 'content-type': 'text/html' })
329
+ return res.end(PAGE)
330
+ }
331
+ if (req.method === 'GET' && req.url === '/api/state') {
332
+ const apps = await Promise.all(CONFIG.apps.map(async (a) => {
333
+ const healthy = await probe(a)
334
+ const phase = activity.get(a.name) || (healthy ? 'running' : failed.has(a.name) ? 'failed' : 'stopped')
335
+ return {
336
+ name: a.name, method: a.method, port: a.port, phase,
337
+ deploy: !!a.deploy, deployMethod: a.deployMethod || 'none',
338
+ external: healthy && !running.has(a.name),
339
+ developedOnce: developedOnce(a),
340
+ error: lastError.get(a.name) || null,
341
+ }
342
+ }))
343
+ return json(res, 200, { currentBranch: currentBranch(), apps })
344
+ }
345
+ const m = req.url && req.url.match(/^\/api\/apps\/([^/]+)\/(build|start|stop|deploy)$/)
346
+ if (m && req.method === 'POST') {
347
+ const app = CONFIG.apps.find((a) => a.name === decodeURIComponent(m[1]))
348
+ if (!app) return json(res, 404, { error: 'unknown app' })
349
+ if (m[2] !== 'stop' && developedOnce(app) === false) {
350
+ return json(res, 409, { error: 'application has not completed a develop workflow run' })
351
+ }
352
+ if (m[2] === 'build') buildApp(app)
353
+ else if (m[2] === 'start') startApp(app)
354
+ else if (m[2] === 'deploy') deployApp(app)
355
+ else stopApp(app)
356
+ return json(res, 200, { ok: true })
357
+ }
358
+ json(res, 404, { error: 'not found' })
359
+ })
360
+
361
+ // WebSocket endpoint /ws?app=<name> — push-only log stream (see "Log streaming" above).
362
+ server.on('upgrade', (req, socket) => {
363
+ let name = ''
364
+ let isWsPath = false
365
+ try {
366
+ const url = new URL(req.url, 'http://x')
367
+ isWsPath = url.pathname === '/ws'
368
+ name = url.searchParams.get('app') || ''
369
+ } catch {}
370
+ const key = req.headers['sec-websocket-key']
371
+ const app = CONFIG.apps.find((a) => a.name === name)
372
+ if (!app || !key || !isWsPath) { socket.destroy(); return }
373
+ const accept = createHash('sha1').update(key + WS_GUID).digest('base64')
374
+ socket.write(
375
+ 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n' +
376
+ `Sec-WebSocket-Accept: ${accept}\r\n\r\n`,
377
+ )
378
+ let subs = subscribers.get(name)
379
+ if (!subs) { subs = new Set(); subscribers.set(name, subs) }
380
+ subs.add(socket)
381
+ const cleanup = () => { subs.delete(socket); try { socket.destroy() } catch {} }
382
+ socket.on('close', cleanup)
383
+ socket.on('error', cleanup)
384
+ // Client frames are only ever close (0x8) — anything else is ignored, never buffered.
385
+ socket.on('data', (buf) => { if ((buf[0] & 0x0f) === 0x8) cleanup() })
386
+ socket.write(wsFrame(logTail(name)))
387
+ })
388
+
389
+ server.listen(PORT, () => console.log(`cicd app on :${PORT}`))
390
+ ```
391
+
392
+ `cicd/app/cicd.config.json` shape — one entry per application from the plan's Deployment
393
+ Decisions table. `deployMethod`/`deploy` come from the plan's Deploy columns (`deploy` is
394
+ `null` when the method is `none`):
395
+
396
+ ```json
397
+ {
398
+ "port": 4100,
399
+ "apps": [
400
+ {
401
+ "name": "my-app",
402
+ "folder": "my-app",
403
+ "method": "process",
404
+ "port": 3000,
405
+ "build": "pnpm install && pnpm build",
406
+ "start": "node dist/index.js",
407
+ "stop": "process-kill",
408
+ "deployMethod": "kubernetes",
409
+ "deploy": "kubectl apply -f my-app/k8s/",
410
+ "health": "http://127.0.0.1:3000/"
411
+ }
412
+ ]
413
+ }
414
+ ```
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: util-plancicd
3
+ model: claude-sonnet-5
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. The plan carries a top-level
14
+ `**Status**: IN PROGRESS` header flipped to `COMPLETED` when done — the studio's watcher reads
15
+ it. This skill NEVER generates the CI/CD app (that is util-gencicdscript, which runs only
16
+ after the studio stamps an **Approved**: line into the plan). Trigger on keywords: "plan
17
+ cicd", "plan deployment", "cicd plan", "infer deployment", "deployment plan", "plan ci/cd".
18
+ Accepts one MANDATORY argument: the project-relative path of the plan-request markdown the
19
+ studio generated (e.g., /util-plancicd execution/cicd-plan-3-1753000000000.md).
20
+ ---
21
+
22
+ # Util Plan CI/CD
23
+
24
+ ## Inputs
25
+
26
+ ```
27
+ /util-plancicd <path-to-plan-request-md>
28
+ ```
29
+
30
+ | Argument | Required | Description |
31
+ |---|---|---|
32
+ | path | yes | Project-relative path of the studio-generated plan-request markdown under `execution/` |
33
+
34
+ If the argument is missing or the file does not exist, STOP and tell the user to click
35
+ **Plan deployment** in the studio's CI/CD screen first.
36
+
37
+ ### Auto-Resolved Paths
38
+
39
+ | Logical file | Path |
40
+ |---|---|
41
+ | Plan request | `<argument>` |
42
+ | Environment | `ENVIRONMENT.md` (project root) |
43
+ | Dev tools | `DEVTOOL.md` (project root) |
44
+ | Project context | `CLAUDE.md` (project root; `source/CLAUDE.md` fallback) |
45
+ | App specification | `<app_folder>/context/specification/SPECIFICATION.md` |
46
+ | Output plan | `cicd/CICD_PLAN.md` |
47
+
48
+ ## Phase 1 — Gather
49
+
50
+ 1. Read the plan-request markdown; its `## Applications` list is the authoritative application set.
51
+ 2. Read `ENVIRONMENT.md` (registries, credentials, external services) and `DEVTOOL.md`
52
+ (installed tools + paths: node/pnpm/docker/kubectl/python versions).
53
+ 3. Read `CLAUDE.md`: `# Custom Applications` (folder names, Depends on) and `# Port Allocation`
54
+ (per-application ports).
55
+ 4. For each application, read its SPECIFICATION.md tech-stack section if present; otherwise
56
+ infer the stack from build files (`package.json` → node, `pom.xml` → spring-boot,
57
+ `composer.json` → laravel).
58
+
59
+ ## Phase 2 — Decide (per application)
60
+
61
+ - **Working directory (CRITICAL)**: the generated CI/CD app executes every command with the
62
+ **application folder** (`<project>/<folder>`) as its working directory — NEVER the project
63
+ root. All paths inside build/start/stop/deploy commands must therefore be relative to the
64
+ application folder (`mvn -q package`, `java -jar target\app.jar`) — never prefixed with the
65
+ application folder itself (`skolafund_middleware\target\...` is WRONG).
66
+ - **Method**: `container` only when Docker appears in DEVTOOL.md AND the stack has a sensible
67
+ container path; otherwise `process` (bare local process — the default for a dev VM).
68
+ - **Build command**: from the stack (e.g. `pnpm install && pnpm build`, `mvn -q package`,
69
+ `composer install`). Use tool paths from DEVTOOL.md when the tool is not on PATH.
70
+ - **Start command**: the stack's production-ish local start (e.g. `node dist/index.js`,
71
+ `java -jar target/<artifact>.jar`, `php artisan serve --port <port>`); `docker build`/
72
+ `docker run` pair when method=container. For jar-based stacks, resolve the EXACT artifact
73
+ filename — read the pom/gradle build config: a Maven `<finalName>` means
74
+ `target/<finalName>.jar` with NO version suffix (Spring Boot repackage keeps that name and
75
+ leaves a `.jar.original` beside it — never reference the `.original`). Never guess
76
+ `<app>-<version>.jar` and never use a `target/*.jar` glob (shell-dependent, breaks on
77
+ Windows cmd).
78
+ - **Stop**: `process-kill` for bare processes; `docker stop <name>` for containers.
79
+ - **Deploy** (method + command): only when ENVIRONMENT.md/DEVTOOL.md show a deployment target —
80
+ `kubernetes` (`kubectl apply -f <app>/k8s/` when kubectl + a cluster/kubeconfig appear),
81
+ `docker` (`docker run`/`docker compose up -d` when the image is the deliverable),
82
+ `web-server` (copy the built artifact into the configured web/app server, e.g. nginx html
83
+ root or Tomcat webapps, when one is configured). Default **`none`** — a bare local process
84
+ needs no deploy step beyond Start; never invent a target the config does not name.
85
+ - **Port**: from `# Port Allocation`; if absent, assign sequentially from 3000 avoiding
86
+ collisions with the studio (3001) and the CI/CD app port.
87
+ - **Health**: `http://127.0.0.1:<port>/` or a documented health endpoint from the spec.
88
+ - **CI/CD app port**: 4100 unless taken in `# Port Allocation`; record it in the plan.
89
+
90
+ ## Phase 3 — Write the plan
91
+
92
+ Create `cicd/` if missing and write `cicd/CICD_PLAN.md`:
93
+
94
+ ```markdown
95
+ # CI/CD Deployment Plan — <Project Name>
96
+
97
+ **Generated**: <ISO date>
98
+ **Status**: IN PROGRESS
99
+
100
+ ## CI/CD App
101
+
102
+ - **Port**: <port>
103
+ - **Folder**: cicd/app
104
+
105
+ ## Deployment Decisions
106
+
107
+ | Application | Folder | Method | Port | Build | Start | Stop | Deploy Method | Deploy | Health |
108
+ |---|---|---|---|---|---|---|---|---|---|
109
+ | <name> | <folder> | process|container | <port> | `<cmd>` | `<cmd>` | process-kill|docker stop | none|web-server|docker|kubernetes | `<cmd or ->` | <url> |
110
+
111
+ ## Rationale
112
+
113
+ - <per-application: why this method/commands, citing ENVIRONMENT.md/DEVTOOL.md evidence>
114
+ ```
115
+
116
+ Then flip `**Status**: IN PROGRESS` to `**Status**: COMPLETED`. The studio watcher advances
117
+ only on COMPLETED.
118
+
119
+ ## STOP — Hand Back to the Human
120
+
121
+ Print a one-paragraph summary and STOP. Never invoke util-gencicdscript or any other skill —
122
+ the human reviews and approves the plan in the studio's CI/CD screen first.
123
+
124
+ ## Important Rules
125
+
126
+ - Never write the CI/CD app; never modify PRD.md/CLAUDE.md/BUG.md.
127
+ - Never delete an existing `**Approved**:` line if re-planning over a previous plan — overwrite
128
+ the whole file WITHOUT any Approved line (a re-plan always requires fresh approval).
129
+ - Credentials from ENVIRONMENT.md may be referenced in commands verbatim (dev-environment
130
+ plaintext rule) but never echoed into the Rationale narrative.