@rashidee/co2 1.3.17 → 1.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.17",
6812
+ version: "1.3.18",
6813
6813
  description: "Compound Context Studio \u2014 self-hosted team context authoring for CO2 projects",
6814
6814
  type: "module",
6815
6815
  license: "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rashidee/co2",
3
- "version": "1.3.17",
3
+ "version": "1.3.18",
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. 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
24
+ running — even ones started outside this app. There is NO version selector and there are NO
25
+ Build All / Start All / Stop All buttons every control operates per application on the
26
+ current working tree, and the plan's commands run verbatim (no {VERSION} or other token
27
+ substitution) — docker images are tagged <repo>/<app>:latest when the plan's Docker section
28
+ is active (managed Dockerfiles materialized before Build when missing; Build never
29
+ pushes). 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
@@ -75,9 +75,8 @@ Read `references/cicd-app-template.md` for the complete file templates. Produce:
75
75
  `{ "path": "Dockerfile", "content": "<body>" }` for a `managed` Dockerfile from the plan's
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
- Commands may carry the literal `{VERSION}` tokencopy it verbatim; the generated app
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
78
+ Commands are copied verbatim from the plan and run as-is there is NO `{VERSION}` token
79
+ or any other placeholder substitution. `deployMethod` is the plan's Deploy Method column
81
80
  (`none|web-server|docker|kubernetes`) and `deploy` its command (`null` when the method is
82
81
  `none`). The `infra` array comes from the plan's `## Infrastructure` table (one entry per
83
82
  row); `url` is `null` when the Console URL cell is `-`. Emit `"infra": []` when the plan's
@@ -164,22 +163,15 @@ port, application count) and STOP — the studio starts/embeds the app.
164
163
  (`<project>/<folder>`) as working directory — command paths are relative to the application
165
164
  folder (e.g. `java -jar target\app.jar`), never to the project root. Deploy renders only
166
165
  when the plan assigned a deploy method other than `none`.
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.
166
+ - **NO version UI, NO All-apps buttons, NO command substitution**: the page carries NO
167
+ version dropdown or any other version choice, and NO Build All / Start All / Stop All
168
+ buttonsBuild/Start/Stop/Deploy are per-application only and always operate on the
169
+ CURRENT working tree, running the plan's commands verbatim (no `{VERSION}` or other token
170
+ substitution). Never render a version control and never check out code.
174
171
  - **Dockerfile materialization**: before Build, when an app's `dockerfile.content` is set and
175
172
  no file exists at its path in the application folder, the server writes it (never
176
173
  overwriting an existing Dockerfile — mirror of the managed-config rule). Build never
177
174
  pushes an image; pushing belongs to a `docker` deploy command.
178
- - **All-apps controls**: the top bar carries **Build All**, **Start All** and **Stop All**
179
- buttons. Build All builds eligible apps SEQUENTIALLY (one at a time — honest logs, no CPU
180
- storm); Start All starts every eligible stopped app; Stop All stops every running app.
181
- Eligible = develop-gate not failed, not busy, and (for build/start) not already running;
182
- ineligible apps are skipped silently — per-tab badges show progress.
183
175
  - Develop gate: controls stay disabled for an application that never completed a
184
176
  conductor-develop run — checked via `${CO2_STUDIO_URL}/api/cicd-guard/${CO2_PROJECT_ID}`
185
177
  with the CO2_GUARD_TOKEN from the spawn env (refreshed every 30 s, last answer cached).
@@ -66,7 +66,6 @@ function currentBranch() {
66
66
  // its tab controls are disabled. Refreshed every 30 s; the last known answer is kept when the
67
67
  // studio is briefly unreachable, and `null` (never fetched) disables controls with a hint.
68
68
  let guardApps = null // Map lower(name|folder) -> developedOnce
69
- let guardVersions = [] // project versions (semver strings, studio order — latest first)
70
69
  async function refreshGuard() {
71
70
  if (!PROJECT_ID) return
72
71
  try {
@@ -77,8 +76,6 @@ async function refreshGuard() {
77
76
  guardApps = new Map()
78
77
  for (const a of g.applications) guardApps.set(String(a.name).toLowerCase(), !!a.developedOnce)
79
78
  }
80
- // The topbar version selector's choices — the {VERSION} tag substituted into commands.
81
- if (Array.isArray(g.versions)) guardVersions = g.versions.map((v) => String(v.semver)).filter(Boolean)
82
79
  } catch {}
83
80
  }
84
81
  function developedOnce(app) {
@@ -229,13 +226,6 @@ function tcpProbe(host, port, timeout = 1000) {
229
226
  })
230
227
  }
231
228
 
232
- // {VERSION} token substitution — the version comes from the page's topbar selector (the
233
- // studio guard's versions, latest preselected). Tag-only: never checks out code. The `v`
234
- // prefix is stripped so docker tags read `1.2.0`, not `v1.2.0`.
235
- function subst(cmd, version) {
236
- return String(cmd || '').replaceAll('{VERSION}', String(version || 'latest').replace(/^v/, ''))
237
- }
238
-
239
229
  // Managed Dockerfile from the plan's ## Dockerfiles — written before Build only when the
240
230
  // application folder has none (an existing Dockerfile is never overwritten).
241
231
  function ensureDockerfile(app) {
@@ -249,38 +239,37 @@ function ensureDockerfile(app) {
249
239
  } catch (e) { logLine(app, `dockerfile: FAILED to write — ${e.message}`) }
250
240
  }
251
241
 
252
- function buildApp(app, version, onDone) {
253
- if (activity.has(app.name)) { onDone && onDone(); return }
242
+ function buildApp(app) {
243
+ if (activity.has(app.name)) return
254
244
  activity.set(app.name, 'building'); lastError.delete(app.name)
255
245
  ensureConfigs(app)
256
246
  ensureDockerfile(app)
257
- runLogged(app, subst(app.build, version), (code) => {
247
+ runLogged(app, app.build, (code) => {
258
248
  activity.delete(app.name)
259
249
  if (code !== 0) lastError.set(app.name, `build exited ${code} — see log`)
260
250
  else logLine(app, 'build OK')
261
- onDone && onDone()
262
251
  })
263
252
  }
264
253
 
265
- function deployApp(app, version) {
254
+ function deployApp(app) {
266
255
  if (activity.has(app.name) || !app.deploy) return
267
256
  activity.set(app.name, 'deploying'); lastError.delete(app.name)
268
- runLogged(app, subst(app.deploy, version), (code) => {
257
+ runLogged(app, app.deploy, (code) => {
269
258
  activity.delete(app.name)
270
259
  if (code !== 0) lastError.set(app.name, `deploy exited ${code} — see log`)
271
260
  else logLine(app, `deploy OK (${app.deployMethod})`)
272
261
  })
273
262
  }
274
263
 
275
- function startApp(app, version) {
264
+ function startApp(app) {
276
265
  if (running.get(app.name) || activity.has(app.name)) return
277
266
  failed.delete(app.name); lastError.delete(app.name)
278
267
  ensureConfigs(app)
279
268
  if (app.method === 'container') {
280
- runLogged(app, subst(app.start, version), () => {})
269
+ runLogged(app, app.start, () => {})
281
270
  saveState(); return
282
271
  }
283
- const child = runLogged(app, subst(app.start, version), (code) => {
272
+ const child = runLogged(app, app.start, (code) => {
284
273
  running.delete(app.name)
285
274
  // A user-requested Stop exits nonzero (killed) — that is not a failure.
286
275
  if (code !== 0 && !stopping.delete(app.name)) {
@@ -327,9 +316,9 @@ function killByPort(port) {
327
316
  } catch { return false }
328
317
  }
329
318
 
330
- function stopApp(app, version) {
319
+ function stopApp(app) {
331
320
  if (app.method === 'container' && app.stop && app.stop !== 'process-kill') {
332
- const stopCmd = subst(app.stop, version)
321
+ const stopCmd = app.stop
333
322
  try { execFileSync(stopCmd.split(' ')[0], stopCmd.split(' ').slice(1), { cwd: PROJECT_DIR }) } catch {}
334
323
  } else {
335
324
  const child = running.get(app.name)
@@ -368,7 +357,6 @@ body{font-family:system-ui,sans-serif;margin:0;background:#f6f7f9;color:#1c2128;
368
357
  .badge.running{background:#1c8a4a}.badge.failed{background:#c23b3b}.badge.building,.badge.deploying{background:#c9922a}
369
358
  .url{font-size:12px;color:#1a66d0;text-decoration:underline;word-break:break-all}
370
359
  button{background:#fff;color:#1c2128;border:1px solid #c6cdd6;border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer}
371
- select{background:#fff;color:#1c2128;border:1px solid #c6cdd6;border-radius:6px;padding:3px 6px;font-size:12px}
372
360
  button:hover:enabled{background:#eef1f5}
373
361
  button:disabled{opacity:.4;cursor:default}
374
362
  .notice{padding:4px 12px;font-size:11px;color:#8a5a00;background:#fff7e6;display:none}
@@ -389,10 +377,6 @@ button:disabled{opacity:.4;cursor:default}
389
377
  .cfg pre{background:#0f1216;color:#d7dde4;font-size:11px;padding:8px 10px;border-radius:6px;overflow:auto;max-height:320px;white-space:pre-wrap;margin:4px 0 0}
390
378
  </style></head><body>
391
379
  <div class="top"><strong>CI/CD</strong><span id="branch" class="meta"></span>
392
- <span class="meta">version</span><select id="ver" title="Tag substituted into {VERSION} in commands (docker image tag)"><option value="">latest</option></select>
393
- <button onclick="actAll('build')" title="Build every eligible application, one at a time">Build All</button>
394
- <button onclick="actAll('start')" title="Start every eligible stopped application">Start All</button>
395
- <button onclick="actAll('stop')" title="Stop every running application">Stop All</button>
396
380
  <span style="flex:1"></span><a id="infralink" class="url" href="infra" target="_blank" rel="noreferrer" style="display:none">Infrastructure ↗</a></div>
397
381
  <div class="tabs" id="tabs"></div>
398
382
  <div class="bar" id="bar"></div>
@@ -431,7 +415,7 @@ function renderBar(){
431
415
  <a class="url" href="\${url}" target="_blank" rel="noreferrer">\${url}</a>
432
416
  <span style="flex:1"></span>
433
417
  <button onclick="openConfig()" title="Show this application's runtime configuration files">Config</button>
434
- <button onclick="act('build')" \${!gate||busy||a.phase==='running'?'disabled':''} title="\${a.image?'Builds the app AND the docker image '+a.image+':<version>':'Runs the plan build command'}">Build</button>
418
+ <button onclick="act('build')" \${!gate||busy||a.phase==='running'?'disabled':''} title="\${a.image?'Builds the app AND the docker image '+a.image+':latest':'Runs the plan build command'}">Build</button>
435
419
  <button onclick="act('start')" \${!gate||busy||a.phase==='running'?'disabled':''}>Start</button>
436
420
  <button onclick="act('stop')" \${a.phase!=='running'?'disabled':''}>Stop</button>
437
421
  \${a.deploy?\`<button onclick="act('deploy')" \${!gate||busy?'disabled':''}>Deploy (\${a.deployMethod})</button>\`:''}\`
@@ -461,7 +445,10 @@ function setActive(name){
461
445
  function openWs(name){
462
446
  // Path-relative + scheme-aware so the stream works direct (:5000/) AND behind the studio's
463
447
  // /api/cicd/<id>/app-proxy/ prefix (which tunnels the upgrade), also under HTTPS.
464
- ws = new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+location.pathname.replace(/\/$/,'')+'/ws?app='+encodeURIComponent(name))
448
+ // NO backslash/regex here on purpose: this string is inside server.js's PAGE template literal,
449
+ // where a client-side regex like /\/$/ cooks to //$/ (a line comment) and kills the script.
450
+ var base = location.pathname.endsWith('/') ? location.pathname.slice(0, -1) : location.pathname
451
+ ws = new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+base+'/ws?app='+encodeURIComponent(name))
465
452
  ws.onmessage = e => appendLog(e.data)
466
453
  ws.onclose = () => { ws = null; if (active === name) wsTimer = setTimeout(() => openWs(name), 2000) }
467
454
  }
@@ -501,35 +488,17 @@ async function openConfig(){
501
488
  function closeConfig(){ document.getElementById('cfgModal').classList.remove('open') }
502
489
  document.addEventListener('keydown', e => { if (e.key === 'Escape') closeConfig() })
503
490
 
504
- // Topbar version selector — the studio guard's project versions (latest first). Repopulated
505
- // only when the list changes so the user's selection survives the 3 s poll.
506
- let verChoices = ''
507
- function renderVersions(versions){
508
- const sel = document.getElementById('ver')
509
- const list = (versions && versions.length ? versions : []).join('|')
510
- if (list === verChoices) return
511
- verChoices = list
512
- sel.innerHTML = (versions && versions.length ? versions : ['latest'])
513
- .map((v, i) => '<option value="'+esc(v)+'"'+(i===0?' selected':'')+'>'+esc(v)+'</option>').join('')
514
- }
515
- function selectedVersion(){ return document.getElementById('ver').value || '' }
516
-
517
491
  async function refresh(){
518
492
  const s = await fetch('api/state').then(r => r.json()).catch(() => null)
519
493
  if (!s) return
520
494
  state = s
521
495
  document.getElementById('branch').textContent = s.currentBranch ? 'branch: '+s.currentBranch : ''
522
496
  document.getElementById('infralink').style.display = s.hasInfra ? 'inline' : 'none'
523
- renderVersions(s.versions)
524
497
  if (!active && s.apps.length) setActive(s.apps[0].name)
525
498
  renderTabs(); renderBar()
526
499
  }
527
500
  async function act(op){
528
- await fetch('api/apps/'+encodeURIComponent(active)+'/'+op+'?version='+encodeURIComponent(selectedVersion()), { method: 'POST' })
529
- refresh()
530
- }
531
- async function actAll(op){
532
- await fetch('api/all/'+op+'?version='+encodeURIComponent(selectedVersion()), { method: 'POST' })
501
+ await fetch('api/apps/'+encodeURIComponent(active)+'/'+op, { method: 'POST' })
533
502
  refresh()
534
503
  }
535
504
  // Rescan immediately when the user returns to this page (tab focus / regained visibility), so
@@ -614,7 +583,6 @@ const server = http.createServer(async (req, res) => {
614
583
  return json(res, 200, {
615
584
  currentBranch: currentBranch(),
616
585
  hasInfra: (loadConfig().infra || []).length > 0,
617
- versions: guardVersions,
618
586
  apps,
619
587
  })
620
588
  }
@@ -624,10 +592,6 @@ const server = http.createServer(async (req, res) => {
624
592
  if (!app) return json(res, 404, { error: 'unknown app' })
625
593
  return json(res, 200, { configs: (app.configs || []).map((c) => readConfig(app, c)) })
626
594
  }
627
- // ?version=<semver> on every action — the topbar selection, substituted into {VERSION}.
628
- const reqVersion = (() => {
629
- try { return new URL(req.url, 'http://x').searchParams.get('version') || '' } catch { return '' }
630
- })()
631
595
  const path = (req.url || '').split('?')[0]
632
596
  const m = path.match(/^\/api\/apps\/([^/]+)\/(build|start|stop|deploy)$/)
633
597
  if (m && req.method === 'POST') {
@@ -636,42 +600,12 @@ const server = http.createServer(async (req, res) => {
636
600
  if (m[2] !== 'stop' && developedOnce(app) === false) {
637
601
  return json(res, 409, { error: 'application has not completed a develop workflow run' })
638
602
  }
639
- if (m[2] === 'build') buildApp(app, reqVersion)
640
- else if (m[2] === 'start') startApp(app, reqVersion)
641
- else if (m[2] === 'deploy') deployApp(app, reqVersion)
642
- else stopApp(app, reqVersion)
603
+ if (m[2] === 'build') buildApp(app)
604
+ else if (m[2] === 'start') startApp(app)
605
+ else if (m[2] === 'deploy') deployApp(app)
606
+ else stopApp(app)
643
607
  return json(res, 200, { ok: true })
644
608
  }
645
- // All-apps controls. Eligible = develop gate not failed and not mid-build/deploy; build and
646
- // start additionally skip apps already running. Builds run SEQUENTIALLY (honest logs, no
647
- // CPU storm); start/stop fire together. Ineligible apps are skipped silently.
648
- const all = path.match(/^\/api\/all\/(build|start|stop)$/)
649
- if (all && req.method === 'POST') {
650
- const op = all[1]
651
- const apps = loadConfig().apps
652
- if (op === 'stop') {
653
- for (const a of apps) {
654
- if (running.has(a.name)) stopApp(a, reqVersion)
655
- else probe(a).then((up) => { if (up) stopApp(a, reqVersion) })
656
- }
657
- return json(res, 200, { ok: true })
658
- }
659
- const eligible = []
660
- for (const a of apps) {
661
- if (developedOnce(a) === false || activity.has(a.name)) continue
662
- if (await probe(a)) continue // already running — skip for build AND start
663
- eligible.push(a)
664
- }
665
- if (op === 'start') {
666
- for (const a of eligible) startApp(a, reqVersion)
667
- return json(res, 200, { ok: true, count: eligible.length })
668
- }
669
- // op === 'build' — sequential chain: each build starts when the previous one exits.
670
- const queue = [...eligible]
671
- const next = () => { const a = queue.shift(); if (a) buildApp(a, reqVersion, next) }
672
- next()
673
- return json(res, 200, { ok: true, count: eligible.length })
674
- }
675
609
  json(res, 404, { error: 'not found' })
676
610
  })
677
611
 
@@ -718,9 +652,8 @@ Docker fields (from the plan's `## Docker` / `## Dockerfiles` sections): top-lev
718
652
  `"dockerfile"` is `{ "path": "Dockerfile", "content": "<managed body>" }` for a `managed`
719
653
  Dockerfile (materialized before Build when missing — never overwriting), or
720
654
  `{ "path": "Dockerfile", "content": null }` for an `existing` one (`null` entirely when the
721
- app has no image). Commands may carry the literal `{VERSION}` token — copied verbatim; the
722
- server substitutes the topbar-selected version (semver sans `v`, fallback `latest`) at
723
- execution time.
655
+ app has no image). Commands are copied verbatim from the plan and run as-is — no `{VERSION}`
656
+ or any other token substitution.
724
657
 
725
658
  Each app also carries a `configs` array — the runtime config files the application reads at start,
726
659
  from the plan's `## Application Configs` section (one entry per file). Fields:
@@ -743,7 +676,7 @@ verbatim (this is a local dev CI/CD app).
743
676
  "folder": "my-app",
744
677
  "method": "process",
745
678
  "port": 3000,
746
- "build": "pnpm install && pnpm build && docker build -t registry.example.com/team/my-app:{VERSION} .",
679
+ "build": "pnpm install && pnpm build && docker build -t registry.example.com/team/my-app:latest .",
747
680
  "start": "node dist/index.js",
748
681
  "stop": "process-kill",
749
682
  "deployMethod": "kubernetes",
@@ -13,8 +13,8 @@ description: >
13
13
  port for the generated CI/CD app itself. When Docker AND a docker repo/registry are both
14
14
  configured (DEVTOOL.md + ENVIRONMENT.md), it additionally plans the docker-image build path:
15
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
16
+ `docker build -t <image>:latest` (no version token the generated CI/CD app runs commands
17
+ verbatim on the current working tree), and a managed Dockerfile authored into the plan's
18
18
  `## Dockerfiles` section for any dockerized application lacking one (materialized before
19
19
  Build, never overwriting an existing file). It also infers each application's RUNTIME CONFIG — the
20
20
  config files its start command reads (e.g. `.env`), deciding per file whether the values come
@@ -124,13 +124,12 @@ If the argument is missing or the file does not exist, STOP and tell the user to
124
124
  any character docker rejects replaced by `-` (keep a leading `<number>_` prefix as-is;
125
125
  e.g. `registry.example.com/team/skolafund_web`).
126
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.
127
+ `<app build> && docker build -t <image>:latest .` Commands carry NO `{VERSION}` token or
128
+ any other placeholder — the generated CI/CD app runs them verbatim; only the current
129
+ working tree is ever built and images are always tagged `latest`.
131
130
  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.
131
+ - **Container start/deploy**: when method=container, reference `<image>:latest` in the
132
+ start/deploy commands too, so the freshly built tag is what runs.
134
133
  - **Dockerfile**: if the application folder already has a `Dockerfile`, use it as-is
135
134
  (`existing`). Otherwise author a stack-appropriate Dockerfile (node / spring-boot /
136
135
  laravel — same stack detection as depgen-k8s) into the plan's `## Dockerfiles` section
@@ -221,12 +220,6 @@ inner ```env config block renders — the plan file itself uses a normal 3-backt
221
220
  > **Health** host — so an app is detected whichever family it binds. Use a `localhost` Health host
222
221
  > for Node/Vite/Next apps (they bind IPv6 `::1`) and `127.0.0.1` for JVM apps.
223
222
 
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
223
  ## Docker
231
224
 
232
225
  Written only from Gather 2a. When Docker or a docker repo is absent, write the section as: