@rashidee/co2 1.2.6 → 1.3.2

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 (90) hide show
  1. package/dist/.co2-dat/app.db +0 -0
  2. package/dist/.co2-dat/app.db-shm +0 -0
  3. package/dist/.co2-dat/app.db-wal +0 -0
  4. package/dist/.co2-dat/config.json +4 -0
  5. package/dist/index.js +5303 -3139
  6. package/drizzle/0011_workflow_history_v152.sql +5 -0
  7. package/drizzle/0012_prd_sections_v160.sql +14 -0
  8. package/drizzle/0013_brainstorm_sessions_v170.sql +10 -0
  9. package/drizzle/0014_bug_module_v180.sql +4 -0
  10. package/drizzle/0015_project_config_v190.sql +23 -0
  11. package/drizzle/0016_cicd_v2100.sql +22 -0
  12. package/drizzle/meta/_journal.json +42 -0
  13. package/package.json +1 -1
  14. package/plugin/README.md +2 -0
  15. package/plugin/SKILLS.md +417 -350
  16. package/plugin/skills/conductor-feature-develop/SKILL.md +1 -1
  17. package/plugin/skills/conductor-feature-prepare/SKILL.md +23 -3
  18. package/plugin/skills/depgen-k8s/SKILL.md +1 -1
  19. package/plugin/skills/tracegen-matrix/SKILL.md +1 -1
  20. package/plugin/skills/util-gencicdscript/SKILL.md +91 -0
  21. package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +414 -0
  22. package/plugin/skills/util-plancicd/SKILL.md +130 -0
  23. package/plugin/skills/util-projectinit/SKILL.md +1 -1
  24. package/plugin/skills/util-projectsync/SKILL.md +1 -1
  25. package/plugin/skills/util-updprd/SKILL.md +85 -15
  26. package/plugin/skills/util-usanalyzer/SKILL.md +1 -1
  27. package/plugin/skills/util-ustagger/SKILL.md +1 -1
  28. package/static/assets/{abnfDiagram-VRR7QNED-DvPuwyCV.js → abnfDiagram-VRR7QNED-CN4J6j_u.js} +1 -1
  29. package/static/assets/{arc-DRKw7sjs.js → arc-N4fM3ZWM.js} +1 -1
  30. package/static/assets/{architectureDiagram-ZJ3FMSHR-CaNcxUbC.js → architectureDiagram-ZJ3FMSHR-uch_sUft.js} +1 -1
  31. package/static/assets/{blockDiagram-677ZJIJ3-Demd4AOK.js → blockDiagram-677ZJIJ3-B8ciSHmI.js} +1 -1
  32. package/static/assets/{c4Diagram-LMCZKHZV-PVKUfc2x.js → c4Diagram-LMCZKHZV-CQcm-uRR.js} +1 -1
  33. package/static/assets/channel-D1lJ_RG1.js +1 -0
  34. package/static/assets/{chunk-2Q5K7J3B-CExf8ww_.js → chunk-2Q5K7J3B-CKyyGyMe.js} +1 -1
  35. package/static/assets/{chunk-32BRIVSS-CFPaoONF.js → chunk-32BRIVSS-BUvhUzbF.js} +1 -1
  36. package/static/assets/{chunk-5VM5RSS4-DE2esM67.js → chunk-5VM5RSS4-DeYPG9gz.js} +1 -1
  37. package/static/assets/{chunk-EX3LRPZG-SON_YdDG.js → chunk-EX3LRPZG-M5DW3NIE.js} +1 -1
  38. package/static/assets/{chunk-JWPE2WC7-Qxah8LNr.js → chunk-JWPE2WC7-DdUQGRCV.js} +1 -1
  39. package/static/assets/{chunk-MOJQB5TN-BcWGQV3j.js → chunk-MOJQB5TN-BjTWQfa-.js} +1 -1
  40. package/static/assets/{chunk-RYQCIY6F-DkDj-OR5.js → chunk-RYQCIY6F-B6eqE68w.js} +1 -1
  41. package/static/assets/{chunk-V7JOEXUC-BlI1Mrah.js → chunk-V7JOEXUC-31fpjmeh.js} +1 -1
  42. package/static/assets/{chunk-VR4S4FIN-Agf__duj.js → chunk-VR4S4FIN-DIdhgHxJ.js} +1 -1
  43. package/static/assets/{chunk-XXDRQBXY-B5CfAh85.js → chunk-XXDRQBXY-CwmvQsRd.js} +1 -1
  44. package/static/assets/classDiagram-OUVF2IWQ-B2LV6Dty.js +1 -0
  45. package/static/assets/classDiagram-v2-EOCWNBFH-B2LV6Dty.js +1 -0
  46. package/static/assets/{cose-bilkent-JH36ORCC-DO0cQkcy.js → cose-bilkent-JH36ORCC-0fxMNvxl.js} +1 -1
  47. package/static/assets/{cynefin-VYW2F7L2-CfOnsnXc.js → cynefin-VYW2F7L2-CykGMMcJ.js} +1 -1
  48. package/static/assets/{cynefinDiagram-TSTJHNR4-BJ8tULAv.js → cynefinDiagram-TSTJHNR4-CXA1U39p.js} +1 -1
  49. package/static/assets/{dagre-VKFMJZFB-DfjwzHqP.js → dagre-VKFMJZFB-Bw5XMtDI.js} +1 -1
  50. package/static/assets/{diagram-FQU43EPY-CndSOIIM.js → diagram-FQU43EPY-xte6N075.js} +1 -1
  51. package/static/assets/{diagram-G47NLZAW-Czf5hN0Z.js → diagram-G47NLZAW-CHoRtp3z.js} +1 -1
  52. package/static/assets/{diagram-NH7WQ7WH-COAyojGh.js → diagram-NH7WQ7WH-CEFlTlPg.js} +1 -1
  53. package/static/assets/{diagram-OA4YK3LP-H8PoiN2m.js → diagram-OA4YK3LP-DJ0JX9s2.js} +1 -1
  54. package/static/assets/{diagram-WEI45ONY-DzcaVzjp.js → diagram-WEI45ONY-BRfCVtnm.js} +1 -1
  55. package/static/assets/{ebnfDiagram-CCIWWBDH-DPazGYLJ.js → ebnfDiagram-CCIWWBDH-Cc-dcKTE.js} +1 -1
  56. package/static/assets/{erDiagram-Q63AITRT-Bo-fubgF.js → erDiagram-Q63AITRT-DLZsu__O.js} +1 -1
  57. package/static/assets/{flowDiagram-23GEKE2U-CSvxUDwh.js → flowDiagram-23GEKE2U-Cczq8RCz.js} +1 -1
  58. package/static/assets/{ganttDiagram-NO4QXBWP-DnBveWX6.js → ganttDiagram-NO4QXBWP-DOXGNB5h.js} +1 -1
  59. package/static/assets/{gitGraphDiagram-IHSO6WYX-B3rVL8c-.js → gitGraphDiagram-IHSO6WYX-BM-SCSXa.js} +1 -1
  60. package/static/assets/{index-y7XDSgMG.js → index-CBLEvyTD.js} +172 -171
  61. package/static/assets/{index-LZSQkLE2.css → index-Dn_JY-18.css} +1 -1
  62. package/static/assets/{infoDiagram-FWYZ7A6U-CvWV9yy2.js → infoDiagram-FWYZ7A6U-dJDRFcA8.js} +1 -1
  63. package/static/assets/{ishikawaDiagram-FXEZZL3T-DF28a1N6.js → ishikawaDiagram-FXEZZL3T-VxLIIU05.js} +1 -1
  64. package/static/assets/{journeyDiagram-5HDEW3XC-CB6CugYk.js → journeyDiagram-5HDEW3XC-EvtzKlzP.js} +1 -1
  65. package/static/assets/{kanban-definition-HUTT4EX6-ResSLF6d.js → kanban-definition-HUTT4EX6-yO62k9Tr.js} +1 -1
  66. package/static/assets/{linear-CLO--ryk.js → linear-DZzMubPw.js} +1 -1
  67. package/static/assets/{mindmap-definition-LN4V7U3C-BvW3EbxX.js → mindmap-definition-LN4V7U3C-BRO6imMR.js} +1 -1
  68. package/static/assets/{pegDiagram-2B236MQR-BHCgqrrM.js → pegDiagram-2B236MQR-DJR3fTw9.js} +1 -1
  69. package/static/assets/{pieDiagram-ENE6RG2P-DVmCQ5FA.js → pieDiagram-ENE6RG2P-9mFM-_RT.js} +1 -1
  70. package/static/assets/{quadrantDiagram-ABIIQ3AL-B3e5KR52.js → quadrantDiagram-ABIIQ3AL-BEsVn0Kx.js} +1 -1
  71. package/static/assets/{railroadDiagram-RFXS5EU6-Csv5ysLc.js → railroadDiagram-RFXS5EU6-D9Mp7Mil.js} +1 -1
  72. package/static/assets/{requirementDiagram-TGXJPOKE-DOMVzvnB.js → requirementDiagram-TGXJPOKE-CRYcxiHN.js} +1 -1
  73. package/static/assets/{sankeyDiagram-HTMAVEWB-B1dcTOvy.js → sankeyDiagram-HTMAVEWB-9MJPBNY-.js} +1 -1
  74. package/static/assets/{sequenceDiagram-DBY2YBRQ-BY414yeO.js → sequenceDiagram-DBY2YBRQ-CSAt7g1a.js} +1 -1
  75. package/static/assets/{sizeCapture-X5ZJPWSS-CuJfrfBN.js → sizeCapture-X5ZJPWSS-DQ7t3sAl.js} +1 -1
  76. package/static/assets/{stateDiagram-2N3HPSRC-FX4rJkzV.js → stateDiagram-2N3HPSRC-B314IVUq.js} +1 -1
  77. package/static/assets/stateDiagram-v2-6OUMAXLB-C5_r7hRR.js +1 -0
  78. package/static/assets/{swimlanes-5IMT3BWC-Sfy58mmv.js → swimlanes-5IMT3BWC-gCXbGpn-.js} +2 -2
  79. package/static/assets/swimlanesDiagram-G3AALYLV-BkEgKJGN.js +8 -0
  80. package/static/assets/{timeline-definition-FHXFAJF6-CmV7by8p.js → timeline-definition-FHXFAJF6-Ypa-zz8f.js} +1 -1
  81. package/static/assets/{vennDiagram-L72KCM5P-FqIyFZFt.js → vennDiagram-L72KCM5P-C07T1tIJ.js} +1 -1
  82. package/static/assets/{wardleyDiagram-EHGQE667-BZ06O45i.js → wardleyDiagram-EHGQE667-DwsMVlwx.js} +1 -1
  83. package/static/assets/{xychartDiagram-FW5EYKEG-BOkffR06.js → xychartDiagram-FW5EYKEG-BCtXbdXL.js} +1 -1
  84. package/static/index.html +2 -2
  85. package/plugin/skills/util-preparek8senv/SKILL.md +0 -422
  86. package/static/assets/channel-C2nVbuwv.js +0 -1
  87. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +0 -1
  88. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +0 -1
  89. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +0 -1
  90. 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,
@@ -551,8 +551,28 @@ to already exist):
551
551
  1. Set the top-level `**Status**:` in `<app_folder>/context/prepare/PREPARE_MASTER.md` to
552
552
  `COMPLETED`, and verify every step and every version row is `COMPLETED` or `SKIPPED` (no
553
553
  `PENDING` / `IN PROGRESS` rows remain)
554
- 2. Output a summary of what was generated/skipped per version (sourced from PREPARE_MASTER.md)
555
- 3. Output `<promise>ALL ARTIFACTS GENERATED</promise>` to signal Ralph Loop completion
554
+ 2. Append an umbrella entry to `CHANGELOG.md` in the application folder
555
+ (`<app_folder>/CHANGELOG.md`) **one entry per version processed**. This is the
556
+ phase-level summary row (mirroring `conductor-feature-develop` / `conductor-defect`); the
557
+ individual sub-skills — `util-ustagger`, `modelgen-*`, `mockgen-*`, `specgen-*`,
558
+ `testgen-functional` — have already appended their own rows during the run, so this row sits
559
+ **on top of** them, it does not replace them:
560
+ - Read `<app_folder>/CHANGELOG.md`. If it does not exist, create it with the standard context
561
+ header (normally the sub-skills already created it earlier in the run).
562
+ - For EACH version in the resolved version list (ascending order):
563
+ - Search for a `## {version}` heading matching this version.
564
+ - If the section **exists**: append a new row to its table (it will exist — the sub-skills
565
+ created it).
566
+ - If the section **does not exist**: insert a new section after the `---` below the context
567
+ header and before any existing `## vX.Y.Z` section (newest-first ordering), with a new
568
+ table header and the first row.
569
+ - Row format: `| {YYYY-MM-DD} | {application_name} | conductor-feature-prepare | {module or "All"} | Prepared context artifacts (model, mockup, spec, test) for {version} — {count} modules |`
570
+ - `{count}` = the number of modules whose artifacts were generated/updated for this version
571
+ (from that version's Artifact Generation Status table in PREPARE_MASTER.md).
572
+ - `{module or "All"}` = the `module` filter if one was active for this run, else `All`.
573
+ - **Never modify or delete existing rows** (including the sub-skills' own rows).
574
+ 3. Output a summary of what was generated/skipped per version (sourced from PREPARE_MASTER.md)
575
+ 4. Output `<promise>ALL ARTIFACTS GENERATED</promise>` to signal Ralph Loop completion
556
576
 
557
577
  **IMPORTANT — Version loop completion**: The completion promise is ONLY output after the LAST
558
578
  version in the resolved list has been fully processed. Do NOT output it after completing an
@@ -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
+ ```