@rtrentjones/greenlight 0.2.19 → 0.2.21
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/README.md +6 -3
- package/assets/skills/provider-hcp/SKILL.md +1 -1
- package/dist/bin.js +73 -15
- package/dist/{chunk-6USV5AQV.js → chunk-GO2RVNOP.js} +23 -4
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/templates/_template-mcp/README.md +1 -1
- package/templates/_template-next/README.md +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,8 @@ greenlight <command>
|
|
|
48
48
|
| `verify <name> --env <beta\|prod>` (or `--url`) | run the shared verify harness |
|
|
49
49
|
| `promote <name>` | gated `develop → main` fast-forward (after beta verify) |
|
|
50
50
|
| `deploy <name>` | target deploy hook (e.g. OCI restart = re-pull) |
|
|
51
|
-
| `
|
|
51
|
+
| `status <name>` | the ship → deploy → verify run chain across repos |
|
|
52
|
+
| `doctor` / `config` | health checks (incl. token-scoping conformance) / load + validate + print the manifest |
|
|
52
53
|
|
|
53
54
|
## The loop
|
|
54
55
|
|
|
@@ -70,7 +71,8 @@ import { defineConfig, defineVerify } from '@rtrentjones/greenlight';
|
|
|
70
71
|
|
|
71
72
|
export default defineConfig({
|
|
72
73
|
domain: 'you.dev',
|
|
73
|
-
|
|
74
|
+
alerts: { sink: 'github-issue' },
|
|
75
|
+
tools: [{ name: 'notes', lane: 'mcp', target: 'oci', data: 'none', auth: 'bearer', envs: ['prod'] }],
|
|
74
76
|
});
|
|
75
77
|
```
|
|
76
78
|
|
|
@@ -80,6 +82,7 @@ Also exported: `loadConfig`, and the `GreenlightConfig` / `VerifySpec` types.
|
|
|
80
82
|
|
|
81
83
|
- **Repo + full docs:** <https://github.com/RTrentJones/greenlight>
|
|
82
84
|
- **Architecture:** [docs/architecture.md](https://github.com/RTrentJones/greenlight/blob/main/docs/architecture.md)
|
|
83
|
-
- **Spec:** [greenlight-
|
|
85
|
+
- **Spec:** [greenlight-v2.md](https://github.com/RTrentJones/greenlight/blob/main/greenlight-v2.md)
|
|
86
|
+
- **Add a provider type:** [docs/adding-a-provider.md](https://github.com/RTrentJones/greenlight/blob/main/docs/adding-a-provider.md)
|
|
84
87
|
|
|
85
88
|
MIT
|
|
@@ -42,5 +42,5 @@ Migrate local → HCP with a plain `terraform init` (answer `yes` to copy state)
|
|
|
42
42
|
plan -out → apply. This is the deploy half — the CLI only edits the `.tf`; CI applies.
|
|
43
43
|
|
|
44
44
|
## Alternatives
|
|
45
|
-
See `docs/terraform-state
|
|
45
|
+
See `docs/terraform-state.md` for the full backend chooser (HCP no-CC · OCI S3-compat ·
|
|
46
46
|
R2 card-required · AWS · local).
|
package/dist/bin.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
loadConfig,
|
|
6
6
|
resolveUrl,
|
|
7
7
|
verifyAll
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-GO2RVNOP.js";
|
|
9
9
|
import "./chunk-HX7VA25D.js";
|
|
10
10
|
import "./chunk-N3IKUCSF.js";
|
|
11
11
|
import "./chunk-KP3Y6WRU.js";
|
|
@@ -58,6 +58,11 @@ function serializeTool(t) {
|
|
|
58
58
|
if (pv.path !== void 0) pvParts.push(`path: ${q(pv.path)}`);
|
|
59
59
|
parts.push(`preview: { ${pvParts.join(", ")} }`);
|
|
60
60
|
}
|
|
61
|
+
if (t.tokens?.length) parts.push(`tokens: [${t.tokens.map(q).join(", ")}]`);
|
|
62
|
+
if (t.tokenOverrides && Object.keys(t.tokenOverrides).length) {
|
|
63
|
+
const ov = Object.entries(t.tokenOverrides).map(([k, v]) => `${k}: ${q(v)}`).join(", ");
|
|
64
|
+
parts.push(`tokenOverrides: { ${ov} }`);
|
|
65
|
+
}
|
|
61
66
|
return ` { ${parts.join(", ")} },`;
|
|
62
67
|
}
|
|
63
68
|
function serializeConfig(c) {
|
|
@@ -103,7 +108,9 @@ function addTool(config, t) {
|
|
|
103
108
|
...t.adopted ? { adopted: true } : {},
|
|
104
109
|
...t.external ? { external: true } : {},
|
|
105
110
|
...t.port !== void 0 ? { port: t.port } : {},
|
|
106
|
-
...t.preview ? { preview: t.preview } : {}
|
|
111
|
+
...t.preview ? { preview: t.preview } : {},
|
|
112
|
+
...t.tokens?.length ? { tokens: t.tokens } : {},
|
|
113
|
+
...t.tokenOverrides && Object.keys(t.tokenOverrides).length ? { tokenOverrides: t.tokenOverrides } : {}
|
|
107
114
|
}
|
|
108
115
|
]
|
|
109
116
|
};
|
|
@@ -129,7 +136,9 @@ function upsertTool(config, t) {
|
|
|
129
136
|
...t.adopted ? { adopted: true } : {},
|
|
130
137
|
...t.external ? { external: true } : {},
|
|
131
138
|
...t.port !== void 0 ? { port: t.port } : {},
|
|
132
|
-
...t.preview ? { preview: t.preview } : {}
|
|
139
|
+
...t.preview ? { preview: t.preview } : {},
|
|
140
|
+
...t.tokens?.length ? { tokens: t.tokens } : {},
|
|
141
|
+
...t.tokenOverrides && Object.keys(t.tokenOverrides).length ? { tokenOverrides: t.tokenOverrides } : {}
|
|
133
142
|
};
|
|
134
143
|
const tools = config.tools.some((x) => x.name === t.name) ? config.tools.map((x) => x.name === t.name ? entry : x) : [...config.tools, entry];
|
|
135
144
|
const result = ConfigSchema.safeParse({ ...config, tools });
|
|
@@ -186,7 +195,9 @@ function resolveEntry(config, name) {
|
|
|
186
195
|
dir: tool.dir ?? `tools/${tool.name}`,
|
|
187
196
|
external: tool.external,
|
|
188
197
|
port: tool.port,
|
|
189
|
-
preview: tool.preview
|
|
198
|
+
preview: tool.preview,
|
|
199
|
+
tokens: tool.tokens,
|
|
200
|
+
tokenOverrides: tool.tokenOverrides
|
|
190
201
|
};
|
|
191
202
|
}
|
|
192
203
|
var VERIFY_MODES = /* @__PURE__ */ new Set(["api", "mcp", "playwright", "test", "agent-web", "eval"]);
|
|
@@ -311,7 +322,7 @@ var PACKS = [
|
|
|
311
322
|
always: true,
|
|
312
323
|
// remote state backs every wrapper's infra
|
|
313
324
|
appliesTo: () => true,
|
|
314
|
-
guide: "docs/terraform-state
|
|
325
|
+
guide: "docs/terraform-state.md \u2014 HCP Terraform free tier (no credit card)",
|
|
315
326
|
setupUrl: "https://app.terraform.io/app/settings/tokens",
|
|
316
327
|
tokens: [
|
|
317
328
|
{
|
|
@@ -401,6 +412,12 @@ var PACKS = [
|
|
|
401
412
|
tfModules: ["tool", "tunnel", "oci-network", "oci-container-instance"]
|
|
402
413
|
}
|
|
403
414
|
];
|
|
415
|
+
function secretKeyFor(tok, toolName, overrides) {
|
|
416
|
+
const override = overrides?.[tok.envVar];
|
|
417
|
+
if (override) return override;
|
|
418
|
+
const suffix = `_${toolName.toUpperCase().replace(/-/g, "_")}`;
|
|
419
|
+
return tok.envVar.toUpperCase() + (tok.perTool ? suffix : "");
|
|
420
|
+
}
|
|
404
421
|
function packsForTool(tool) {
|
|
405
422
|
return PACKS.filter((p) => p.always || (tool ? p.appliesTo(tool) : false));
|
|
406
423
|
}
|
|
@@ -426,7 +443,7 @@ function tokensForTool(tool) {
|
|
|
426
443
|
}
|
|
427
444
|
|
|
428
445
|
// src/version.ts
|
|
429
|
-
var MODULE_REF = "v0.2.
|
|
446
|
+
var MODULE_REF = "v0.2.21";
|
|
430
447
|
var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
|
|
431
448
|
function moduleSource(module, ref = MODULE_REF) {
|
|
432
449
|
return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
|
|
@@ -441,6 +458,7 @@ function emitToolTf(opts) {
|
|
|
441
458
|
const useSupabase = data === "supabase";
|
|
442
459
|
const useVercel = target === "vercel";
|
|
443
460
|
const useOci = target === "oci";
|
|
461
|
+
const supabaseOverride = opts.tokenOverrides?.SUPABASE_ACCESS_TOKEN;
|
|
444
462
|
const envList = envs.map((e) => `"${e}"`).join(", ");
|
|
445
463
|
const blocks = [];
|
|
446
464
|
const assumes = ["var.cloudflare_zone_id"];
|
|
@@ -455,9 +473,25 @@ function emitToolTf(opts) {
|
|
|
455
473
|
# External tool: app code + deploy live in ${slug}; this manages only its infra here.` : ""}`
|
|
456
474
|
);
|
|
457
475
|
if (useSupabase) {
|
|
476
|
+
const providersLine = supabaseOverride ? `
|
|
477
|
+
providers = { supabase = supabase.${name} }` : "";
|
|
478
|
+
const overrideBlock = supabaseOverride ? `
|
|
479
|
+
|
|
480
|
+
# Multi-account: ${name}'s Supabase lives in a SECOND account \u2014 an aliased provider authenticates
|
|
481
|
+
# with its own token. In infra.yml: TF_VAR_${name}_supabase_access_token: \${{ secrets.${supabaseOverride} }}
|
|
482
|
+
provider "supabase" {
|
|
483
|
+
alias = "${name}"
|
|
484
|
+
access_token = var.${name}_supabase_access_token
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
variable "${name}_supabase_access_token" {
|
|
488
|
+
type = string
|
|
489
|
+
sensitive = true
|
|
490
|
+
description = "Supabase Management API token for ${name}'s account (scoped secret ${supabaseOverride})."
|
|
491
|
+
}` : "";
|
|
458
492
|
blocks.push(`# One Supabase project (schema-per-env), kept declarative + recreatable + kept alive.
|
|
459
493
|
module "${name}_supabase" {
|
|
460
|
-
source = "${moduleSource("supabase", ref)}"
|
|
494
|
+
source = "${moduleSource("supabase", ref)}"${providersLine}
|
|
461
495
|
|
|
462
496
|
name = "${name}"
|
|
463
497
|
project_name = "${name}-db"
|
|
@@ -473,7 +507,7 @@ variable "${name}_supabase_database_password" {
|
|
|
473
507
|
type = string
|
|
474
508
|
sensitive = true
|
|
475
509
|
default = "import-placeholder" # ignored when importing an existing project
|
|
476
|
-
}`);
|
|
510
|
+
}${overrideBlock}`);
|
|
477
511
|
}
|
|
478
512
|
if (useVercel) {
|
|
479
513
|
const env = useSupabase ? `
|
|
@@ -643,7 +677,7 @@ locals {
|
|
|
643
677
|
` : "";
|
|
644
678
|
return `# Wrapper infra (singleton): providers + remote-state backend + shared variables.
|
|
645
679
|
# \`greenlight add\` appends per-tool module blocks as infra/<name>.tf. Apply is CI/CD's job
|
|
646
|
-
# (infra.yml). Fill in the HCP backend below before the first apply (docs/terraform-state
|
|
680
|
+
# (infra.yml). Fill in the HCP backend below before the first apply (docs/terraform-state.md).
|
|
647
681
|
|
|
648
682
|
terraform {
|
|
649
683
|
required_version = ">= 1.7"
|
|
@@ -918,8 +952,7 @@ async function gatherSecrets(name, repo, env, prefill) {
|
|
|
918
952
|
for (const pack of packs) {
|
|
919
953
|
console.log(`\u2500\u2500 ${pack.name}${pack.setupUrl ? ` \u2192 ${pack.setupUrl}` : ""}`);
|
|
920
954
|
for (const tok of pack.tokens) {
|
|
921
|
-
const
|
|
922
|
-
const key = tok.envVar.toUpperCase() + (tok.perTool ? suffix : "");
|
|
955
|
+
const key = secretKeyFor(tok, name, entry.tokenOverrides);
|
|
923
956
|
if (key === "GITHUB_TOKEN") {
|
|
924
957
|
console.log(" \xB7 GITHUB_TOKEN \u2014 provided automatically by Actions; skipping");
|
|
925
958
|
continue;
|
|
@@ -1070,7 +1103,16 @@ async function addCommand(args) {
|
|
|
1070
1103
|
} else {
|
|
1071
1104
|
writeFileSync2(
|
|
1072
1105
|
toolTf,
|
|
1073
|
-
emitToolTf({
|
|
1106
|
+
emitToolTf({
|
|
1107
|
+
name,
|
|
1108
|
+
domain: config.domain,
|
|
1109
|
+
lane,
|
|
1110
|
+
target,
|
|
1111
|
+
data,
|
|
1112
|
+
envs,
|
|
1113
|
+
port: entry?.port,
|
|
1114
|
+
tokenOverrides: entry?.tokenOverrides
|
|
1115
|
+
})
|
|
1074
1116
|
);
|
|
1075
1117
|
console.log(`\u2714 wrote infra/${name}.tf (modules: ${providers.join(", ")})`);
|
|
1076
1118
|
}
|
|
@@ -1976,6 +2018,16 @@ function conformanceChecks(t, root) {
|
|
|
1976
2018
|
status: gateable ? "ok" : "warn",
|
|
1977
2019
|
detail: platformPreview ? "vercel per-PR preview + deployment_status verify" : gateable ? void 0 : `no built-in serve for ${t.external ? "an external " : ""}${t.target} tool \u2014 add preview:{ command, \u2026 } so \`greenlight preview ${t.name}\` works`
|
|
1978
2020
|
});
|
|
2021
|
+
const declared = [...t.tokens ?? [], ...Object.values(t.tokenOverrides ?? {})];
|
|
2022
|
+
if (declared.length) {
|
|
2023
|
+
const tag = t.name.toUpperCase().replace(/-/g, "_");
|
|
2024
|
+
const generic = declared.filter((s) => !s.toUpperCase().includes(tag));
|
|
2025
|
+
out.push({
|
|
2026
|
+
name: `${t.name}: token scoping`,
|
|
2027
|
+
status: generic.length ? "warn" : "ok",
|
|
2028
|
+
detail: generic.length ? `not tool-scoped (should contain ${tag}): ${generic.join(", ")}` : `${declared.length} scoped secret name(s)`
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
1979
2031
|
return out;
|
|
1980
2032
|
}
|
|
1981
2033
|
function runDoctor(config, root) {
|
|
@@ -2280,7 +2332,7 @@ async function initCommand(args) {
|
|
|
2280
2332
|
Next:
|
|
2281
2333
|
1. greenlight add <name> --lane <lane> --target <target> # scaffold a tool, emit infra, and
|
|
2282
2334
|
# gather THAT tool's keys \u2192 GitHub${pushed ? "" : "\n (run `greenlight secrets sync` if base tokens were not pushed)"}
|
|
2283
|
-
2. set the HCP backend (cloud{} org + workspace) in infra/main.tf # docs/terraform-state
|
|
2335
|
+
2. set the HCP backend (cloud{} org + workspace) in infra/main.tf # docs/terraform-state.md
|
|
2284
2336
|
3. commit + push \u2192 CI (.github/workflows/infra.yml) runs \`terraform apply\`
|
|
2285
2337
|
4. greenlight verify <name> --env prod | greenlight doctor`);
|
|
2286
2338
|
}
|
|
@@ -2296,7 +2348,13 @@ import { resolve as resolve9 } from "path";
|
|
|
2296
2348
|
function defaultSpec(lane) {
|
|
2297
2349
|
switch (lane) {
|
|
2298
2350
|
case "astro":
|
|
2299
|
-
return {
|
|
2351
|
+
return {
|
|
2352
|
+
mode: "api",
|
|
2353
|
+
checks: [{ path: "/", status: 200 }],
|
|
2354
|
+
noBrokenInternalLinks: true,
|
|
2355
|
+
settleRetries: 8,
|
|
2356
|
+
settleMs: 5e3
|
|
2357
|
+
};
|
|
2300
2358
|
case "next":
|
|
2301
2359
|
return { mode: "api", checks: [{ path: "/", status: 200 }] };
|
|
2302
2360
|
case "mcp":
|
|
@@ -2715,7 +2773,7 @@ var HELP = `greenlight <command>
|
|
|
2715
2773
|
doctor manifest + repo consistency checks
|
|
2716
2774
|
help show this message
|
|
2717
2775
|
|
|
2718
|
-
Real cloud deploys need the target's creds (e.g. CLOUDFLARE_API_TOKEN); see greenlight-v1.md \xA716.`;
|
|
2776
|
+
Real cloud deploys need the target's creds (e.g. CLOUDFLARE_API_TOKEN); see docs/archive/greenlight-v1.md \xA716.`;
|
|
2719
2777
|
async function main() {
|
|
2720
2778
|
const [cmd, ...args] = process.argv.slice(2);
|
|
2721
2779
|
switch (cmd) {
|
|
@@ -41,7 +41,7 @@ var ToolSchema = z.object({
|
|
|
41
41
|
// (poly-repo) tool sets '.' (the repo root).
|
|
42
42
|
dir: z.string().optional(),
|
|
43
43
|
// The tool's code lives in another repo — this entry is a registry pointer only,
|
|
44
|
-
// not built/deployed here (greenlight-v1.md §15.5 poly-repo).
|
|
44
|
+
// not built/deployed here (docs/archive/greenlight-v1.md §15.5 poly-repo).
|
|
45
45
|
external: z.boolean().default(false),
|
|
46
46
|
// How `greenlight preview` spins the tool up LOCALLY for the pre-deploy gate. Optional — node
|
|
47
47
|
// lanes (astro/next/mcp→workers) use the built-in build+serve path. Set it for targets with no
|
|
@@ -56,7 +56,16 @@ var ToolSchema = z.object({
|
|
|
56
56
|
// local port (default: tool.port ?? lane default)
|
|
57
57
|
path: z.string().optional()
|
|
58
58
|
// connect path (default: lane default, e.g. `/mcp`)
|
|
59
|
-
}).optional()
|
|
59
|
+
}).optional(),
|
|
60
|
+
// The project-scoped secret names this tool needs (e.g. ['TF_VAR_HEISTMIND_GITHUB_ADMIN_TOKEN']).
|
|
61
|
+
// The convention (docs/tokens-reference.md): a project-scoped secret carries the uppercased tool name.
|
|
62
|
+
// `doctor` warns on a name that doesn't — documentation + conformance, no behavior.
|
|
63
|
+
tokens: z.array(z.string()).optional(),
|
|
64
|
+
// Opt-in per-tool provider-token OVERRIDES (multi-account). Maps a provider's default token env
|
|
65
|
+
// var to an alternate secret name, so this tool authenticates that provider with a SECOND account
|
|
66
|
+
// — e.g. { SUPABASE_ACCESS_TOKEN: 'SUPABASE_ACCESS_TOKEN_HEISTMIND' }. Absent ⇒ unchanged (the
|
|
67
|
+
// default token). `add`/`adopt` emit an aliased provider + scoped var/secret for an overridden token.
|
|
68
|
+
tokenOverrides: z.record(z.string(), z.string()).optional()
|
|
60
69
|
}).superRefine((tool, ctx) => {
|
|
61
70
|
const rule = MATRIX[tool.lane];
|
|
62
71
|
if (!rule.targets.includes(tool.target)) {
|
|
@@ -197,8 +206,7 @@ async function checkInternalLinks(base, max = 25) {
|
|
|
197
206
|
return { name: "no broken internal links", pass: false, detail: msg(e) };
|
|
198
207
|
}
|
|
199
208
|
}
|
|
200
|
-
async function
|
|
201
|
-
const base = trimSlash(baseUrl);
|
|
209
|
+
async function runChecks(base, spec) {
|
|
202
210
|
const checks = [];
|
|
203
211
|
for (const c of spec.checks ?? []) checks.push(await checkRoute(base, c));
|
|
204
212
|
if (spec.rssValid) {
|
|
@@ -217,6 +225,17 @@ async function verifyApi(baseUrl, spec) {
|
|
|
217
225
|
);
|
|
218
226
|
}
|
|
219
227
|
if (spec.noBrokenInternalLinks) checks.push(await checkInternalLinks(base));
|
|
228
|
+
return checks;
|
|
229
|
+
}
|
|
230
|
+
async function verifyApi(baseUrl, spec) {
|
|
231
|
+
const base = trimSlash(baseUrl);
|
|
232
|
+
const retries = Math.max(0, spec.settleRetries ?? 0);
|
|
233
|
+
const delayMs = spec.settleMs ?? 5e3;
|
|
234
|
+
let checks = await runChecks(base, spec);
|
|
235
|
+
for (let i = 0; i < retries && !checks.every((c) => c.pass); i++) {
|
|
236
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
237
|
+
checks = await runChecks(base, spec);
|
|
238
|
+
}
|
|
220
239
|
return report("api", baseUrl, checks);
|
|
221
240
|
}
|
|
222
241
|
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtrentjones/greenlight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"description": "Greenlight CLI — setup and lifecycle for the harness.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@rtrentjones/greenlight-adapters": "0.2.4",
|
|
35
|
-
"@rtrentjones/greenlight-loop": "0.2.4",
|
|
36
35
|
"@rtrentjones/greenlight-shared": "0.2.4",
|
|
37
|
-
"@rtrentjones/greenlight-verify": "0.2.4"
|
|
36
|
+
"@rtrentjones/greenlight-verify": "0.2.4",
|
|
37
|
+
"@rtrentjones/greenlight-loop": "0.2.4"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/copy-assets.mjs && tsup",
|
|
@@ -25,4 +25,4 @@ export default { mode: 'mcp', expectTools: ['<tool>'], call: { name: '<tool>' }
|
|
|
25
25
|
|
|
26
26
|
## Auth
|
|
27
27
|
|
|
28
|
-
`auth: none` only for public read-only servers. Mutating/private servers default to `bearer`/`oauth` (greenlight-v1.md §6/§14).
|
|
28
|
+
`auth: none` only for public read-only servers. Mutating/private servers default to `bearer`/`oauth` (docs/archive/greenlight-v1.md §6/§14).
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
Lane template for **Next.js on Vercel** with Supabase — verify mode `api + playwright`.
|
|
4
4
|
|
|
5
|
-
> **Phase 0:** placeholder only. Real template content arrives when the `next` lane is exercised (HeistMind migration, **Phase 9** — greenlight-v1.md §16). Materialized into a tool by `greenlight add` / `greenlight adopt`.
|
|
5
|
+
> **Phase 0:** placeholder only. Real template content arrives when the `next` lane is exercised (HeistMind migration, **Phase 9** — docs/archive/greenlight-v1.md §16). Materialized into a tool by `greenlight add` / `greenlight adopt`.
|