engsys 1.0.0
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/LICENSE +21 -0
- package/README.md +202 -0
- package/core/agents/aaron.md +152 -0
- package/core/agents/bert.md +115 -0
- package/core/agents/isabelle.md +136 -0
- package/core/agents/jody.md +150 -0
- package/core/agents/leith.md +111 -0
- package/core/agents/marcelo.md +282 -0
- package/core/agents/melvin.md +101 -0
- package/core/agents/nyx.md +152 -0
- package/core/agents/otto.md +168 -0
- package/core/agents/patricia.md +283 -0
- package/core/commands/design-audit-local.md +155 -0
- package/core/commands/design-audit.md +235 -0
- package/core/commands/design-critique.md +96 -0
- package/core/commands/file-issue.md +22 -0
- package/core/commands/generate-project.md +45 -0
- package/core/commands/implement-issue.md +37 -0
- package/core/commands/implement-project.md +40 -0
- package/core/commands/naturalize.md +61 -0
- package/core/commands/pre-push.md +29 -0
- package/core/commands/prep-review-collect.md +130 -0
- package/core/commands/prep-review-finalize.md +121 -0
- package/core/commands/prep-review-publish.md +113 -0
- package/core/commands/prep-review.md +65 -0
- package/core/commands/project-closeout.md +25 -0
- package/core/skills/agentic-eval/SKILL.md +195 -0
- package/core/skills/chrome-devtools/SKILL.md +97 -0
- package/core/skills/code-review/SKILL.md +26 -0
- package/core/skills/gh-cli/SKILL.md +2202 -0
- package/core/skills/git-commit/SKILL.md +124 -0
- package/core/skills/git-workflow-agents/SKILL.md +462 -0
- package/core/skills/git-workflow-agents/reference.md +220 -0
- package/core/skills/github-actions/SKILL.md +190 -0
- package/core/skills/github-issues/SKILL.md +154 -0
- package/core/skills/llm-structured-outputs/SKILL.md +323 -0
- package/core/skills/llm-structured-outputs/references/provider-details.md +392 -0
- package/core/skills/pre-push/SKILL.md +115 -0
- package/core/skills/refactor/SKILL.md +645 -0
- package/core/skills/web-design-reviewer/SKILL.md +371 -0
- package/core/skills/webapp-testing/SKILL.md +127 -0
- package/core/skills/webapp-testing/test-helper.js +56 -0
- package/core/templates/CLAUDE.md.tmpl +98 -0
- package/core/templates/adr-template.md +67 -0
- package/core/templates/gh-issue-templates/bug.md +39 -0
- package/core/templates/gh-issue-templates/content.md +42 -0
- package/core/templates/gh-issue-templates/enhancement.md +36 -0
- package/core/templates/gh-issue-templates/feature.md +39 -0
- package/core/templates/gh-issue-templates/infrastructure.md +41 -0
- package/core/templates/post-edit-reminders.sh.tmpl +19 -0
- package/core/templates/settings.json.tmpl +90 -0
- package/core/templates/settings.local.json.tmpl +3 -0
- package/core/workflows/agent-implementation-workflow.md +346 -0
- package/core/workflows/generate-project.md +258 -0
- package/core/workflows/implement-project-workflow.md +190 -0
- package/core/workflows/issue-tracking.md +89 -0
- package/core/workflows/project-closeout-ceremony.md +77 -0
- package/core/workflows/review-workflow.md +266 -0
- package/engsys.config.example.yaml +46 -0
- package/install +202 -0
- package/lessons-library/README.md +80 -0
- package/lessons-library/async-callbacks-verify-liveness.md +15 -0
- package/lessons-library/change-isnt-done-until-every-surface-updated.md +15 -0
- package/lessons-library/claim-then-act-for-irreversible-ops.md +16 -0
- package/lessons-library/co-commit-entangled-work.md +15 -0
- package/lessons-library/dependabot-triage-playbook.md +17 -0
- package/lessons-library/deploy-by-digest-and-verify-the-running-revision.md +15 -0
- package/lessons-library/enforce-your-guarantee-at-your-boundary.md +16 -0
- package/lessons-library/gate-changes-on-measurement-not-vibes.md +15 -0
- package/lessons-library/iac-first-no-console-changes.md +15 -0
- package/lessons-library/independent-objective-review-gate.md +15 -0
- package/lessons-library/keep-an-immutable-source-of-truth.md +15 -0
- package/lessons-library/long-agent-runs-checkpoint-not-poll.md +15 -0
- package/lessons-library/model-identity-with-stable-ids-and-provenance.md +15 -0
- package/lessons-library/operator-choices-are-first-class.md +15 -0
- package/lessons-library/prefer-tool-enforced-structured-output.md +15 -0
- package/lessons-library/prove-causation-before-acting.md +15 -0
- package/lessons-library/re-read-state-before-acting.md +14 -0
- package/lessons-library/read-layer-tolerates-unbackfilled-rows.md +15 -0
- package/lessons-library/shell-safety-pipefail-and-validate-before-teardown.md +14 -0
- package/lessons-library/shift-correctness-left-and-distrust-false-greens.md +15 -0
- package/lessons-library/stray-control-bytes-hide-changes.md +14 -0
- package/lessons-library/tests-can-assert-the-bug.md +15 -0
- package/lessons-library/verify-ground-truth-not-reports.md +15 -0
- package/lessons-library/worktrees-need-bootstrap-from-origin-main.md +15 -0
- package/lib/commands.js +356 -0
- package/lib/generate-team-avatars.mjs +251 -0
- package/lib/manifest.js +155 -0
- package/lib/render.js +135 -0
- package/lib/selftest.js +90 -0
- package/lib/util.js +89 -0
- package/lib/yaml.js +156 -0
- package/optional-agents/gary.md +86 -0
- package/optional-agents/jos.md +136 -0
- package/optional-agents/sandy.md +101 -0
- package/optional-agents/steve.md +161 -0
- package/package.json +43 -0
- package/stacks/cloud/aws/claude.fragment.md +17 -0
- package/stacks/cloud/aws/settings.fragment.json +39 -0
- package/stacks/cloud/aws/skills/aws-deployment-preflight/SKILL.md +165 -0
- package/stacks/cloud/aws/skills/cloud-architecture-aws/SKILL.md +265 -0
- package/stacks/cloud/azure/claude.fragment.md +17 -0
- package/stacks/cloud/azure/settings.fragment.json +45 -0
- package/stacks/cloud/azure/skills/azure-deployment-preflight/SKILL.md +175 -0
- package/stacks/cloud/azure/skills/cloud-architecture-azure/SKILL.md +211 -0
- package/stacks/cloud/cloudflare/claude.fragment.md +21 -0
- package/stacks/cloud/cloudflare/settings.fragment.json +31 -0
- package/stacks/cloud/cloudflare/skills/cloud-architecture-cloudflare/SKILL.md +294 -0
- package/stacks/cloud/cloudflare/skills/cloudflare-deployment-preflight/SKILL.md +175 -0
- package/stacks/cloud/gcp/claude.fragment.md +17 -0
- package/stacks/cloud/gcp/settings.fragment.json +40 -0
- package/stacks/cloud/gcp/skills/cloud-architecture-gcp/SKILL.md +208 -0
- package/stacks/cloud/gcp/skills/gcp-deployment-preflight/SKILL.md +137 -0
- package/stacks/db/mongo/skills/mongo-conventions/SKILL.md +96 -0
- package/stacks/db/prisma/claude.fragment.md +49 -0
- package/stacks/db/prisma/skills/docker-database-package-copy/SKILL.md +44 -0
- package/stacks/db/prisma/skills/prisma-conventions/SKILL.md +37 -0
- package/stacks/domain/mobile-growth/skills/apple-ads/SKILL.md +184 -0
- package/stacks/domain/mobile-growth/skills/apple-ads/references/benchmark-notes.md +47 -0
- package/stacks/domain/mobile-growth/skills/apple-ads/references/official-links.md +53 -0
- package/stacks/domain/mobile-growth/skills/google-play-growth/SKILL.md +197 -0
- package/stacks/domain/mobile-growth/skills/google-play-growth/references/benchmark-notes.md +47 -0
- package/stacks/domain/mobile-growth/skills/google-play-growth/references/official-links.md +45 -0
- package/stacks/iac/bicep/claude.fragment.md +14 -0
- package/stacks/iac/bicep/settings.fragment.json +20 -0
- package/stacks/iac/bicep/skills/iac-bicep/SKILL.md +113 -0
- package/stacks/iac/cdk/claude.fragment.md +14 -0
- package/stacks/iac/cdk/settings.fragment.json +23 -0
- package/stacks/iac/cdk/skills/iac-cdk/SKILL.md +104 -0
- package/stacks/iac/terraform/claude.fragment.md +13 -0
- package/stacks/iac/terraform/settings.fragment.json +25 -0
- package/stacks/iac/terraform/skills/iac-terraform/SKILL.md +93 -0
- package/stacks/iac/terraform/skills/terraform-conventions/SKILL.md +87 -0
- package/stacks/lang/kotlin/skills/android-testing/SKILL.md +263 -0
- package/stacks/lang/kotlin/skills/jetpack-compose/SKILL.md +264 -0
- package/stacks/lang/kotlin/skills/kotlin-coroutines/SKILL.md +329 -0
- package/stacks/lang/python/skills/python-conventions/SKILL.md +61 -0
- package/stacks/lang/shell/skills/shell-scripting/SKILL.md +110 -0
- package/stacks/lang/swift/skills/swift-concurrency/SKILL.md +423 -0
- package/stacks/lang/swift/skills/swift-concurrency/references/approachable-concurrency.md +80 -0
- package/stacks/lang/swift/skills/swift-concurrency/references/concurrency-patterns.md +233 -0
- package/stacks/lang/swift/skills/swift-concurrency/references/swiftui-concurrency.md +187 -0
- package/stacks/lang/swift/skills/swift-concurrency/references/synchronization-primitives.md +341 -0
- package/stacks/lang/swift/skills/swift-testing/SKILL.md +497 -0
- package/stacks/lang/swift/skills/swift-testing/references/testing-advanced.md +106 -0
- package/stacks/lang/swift/skills/swift-testing/references/testing-patterns.md +504 -0
- package/stacks/lang/swift/skills/swiftdata/SKILL.md +334 -0
- package/stacks/lang/swift/skills/swiftdata/references/core-data-coexistence.md +504 -0
- package/stacks/lang/swift/skills/swiftdata/references/swiftdata-advanced.md +975 -0
- package/stacks/lang/swift/skills/swiftdata/references/swiftdata-queries.md +675 -0
- package/stacks/lang/swift/skills/swiftui-patterns/SKILL.md +371 -0
- package/stacks/lang/swift/skills/swiftui-patterns/references/architecture-patterns.md +486 -0
- package/stacks/lang/swift/skills/swiftui-patterns/references/deprecated-migration.md +1097 -0
- package/stacks/lang/swift/skills/swiftui-patterns/references/design-polish.md +780 -0
- package/stacks/lang/swift/skills/swiftui-patterns/references/platform-and-sharing.md +696 -0
- package/stacks/lang/typescript/skills/typescript-conventions/SKILL.md +91 -0
- package/stacks/platform/android/claude.fragment.md +40 -0
- package/stacks/platform/android/hooks/pre-push-gradle.sh +70 -0
- package/stacks/platform/android/settings.fragment.json +13 -0
- package/stacks/platform/android/skills/android-build-conventions/SKILL.md +247 -0
- package/stacks/platform/ios/claude.fragment.md +24 -0
- package/stacks/platform/ios/hooks/pre-push-xcodebuild.sh +82 -0
- package/stacks/platform/ios/settings.fragment.json +21 -0
- package/stacks/platform/ios/skills/xcodebuildmcp-simulator-logs/SKILL.md +76 -0
- package/stacks/platform/web/skills/frontend-testing/SKILL.md +246 -0
- package/stacks/platform/web/skills/react-conventions/SKILL.md +261 -0
- package/stacks/platform/web/skills/web-platform-conventions/SKILL.md +55 -0
- package/stacks/tooling/issue-tracker-github/claude.fragment.md +10 -0
- package/stacks/tooling/issue-tracker-github/settings.fragment.json +24 -0
- package/stacks/tooling/issue-tracker-github/skills/issue-tracker-github/SKILL.md +278 -0
- package/stacks/tooling/issue-tracker-linear/claude.fragment.md +17 -0
- package/stacks/tooling/issue-tracker-linear/settings.fragment.json +9 -0
- package/stacks/tooling/issue-tracker-linear/skills/issue-tracker-linear/SKILL.md +183 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate team-roster headshots for engsys agent personas.
|
|
4
|
+
*
|
|
5
|
+
* The image prompt is derived FROM the agent profile itself — the script reads
|
|
6
|
+
* the persona's name, role, and personality straight out of the .md and wraps it
|
|
7
|
+
* in a consistent "house style" art direction. Point it at any agent profile.
|
|
8
|
+
*
|
|
9
|
+
* Config (from .env at the repo root, or the environment):
|
|
10
|
+
* OPENAI_API_KEY required
|
|
11
|
+
* OPENAI_IMAGE_MODEL e.g. gpt-image-1 (default) or dall-e-3
|
|
12
|
+
* OPENAI_IMAGE_QUALITY optional override (gpt-image-1: low|medium|high|auto;
|
|
13
|
+
* dall-e-3: standard|hd)
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node lib/generate-team-avatars.mjs <agent.md> [<agent.md> ...] [options]
|
|
17
|
+
*
|
|
18
|
+
* Options:
|
|
19
|
+
* --count=N how many variations to generate per profile (1-5, default 1)
|
|
20
|
+
* --out=DIR output directory (default: tmp/ at the repo root)
|
|
21
|
+
* --dry-run print the constructed prompt(s) and exit — no API call, no cost
|
|
22
|
+
*
|
|
23
|
+
* Examples:
|
|
24
|
+
* node lib/generate-team-avatars.mjs optional-agents/sandy.md --count=3
|
|
25
|
+
* node lib/generate-team-avatars.mjs core/agents/*.md --count=2
|
|
26
|
+
* node lib/generate-team-avatars.mjs optional-agents/gary.md --dry-run
|
|
27
|
+
*
|
|
28
|
+
* Output files are versioned (sandy-001.png, sandy-002.png, …) and never
|
|
29
|
+
* overwrite existing files. Pick the winner, then move it to team-images/<id>.png.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import fs from 'fs';
|
|
33
|
+
import path from 'path';
|
|
34
|
+
import process from 'node:process';
|
|
35
|
+
import { fileURLToPath } from 'url';
|
|
36
|
+
|
|
37
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
39
|
+
|
|
40
|
+
// --- config / .env ---------------------------------------------------------
|
|
41
|
+
const ENV_PATH = path.join(REPO_ROOT, '.env');
|
|
42
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
43
|
+
try {
|
|
44
|
+
process.loadEnvFile(ENV_PATH); // Node >= 20.12 / 22
|
|
45
|
+
} catch {
|
|
46
|
+
// minimal fallback parser (KEY=VALUE per line, ignores # comments)
|
|
47
|
+
for (const line of fs.readFileSync(ENV_PATH, 'utf8').split(/\r?\n/)) {
|
|
48
|
+
const m = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
|
|
49
|
+
if (m && !line.trimStart().startsWith('#')) {
|
|
50
|
+
const val = m[2].replace(/^["']|["']$/g, '');
|
|
51
|
+
if (!(m[1] in process.env)) process.env[m[1]] = val;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
58
|
+
const MODEL = process.env.OPENAI_IMAGE_MODEL || 'gpt-image-1';
|
|
59
|
+
const API_URL = 'https://api.openai.com/v1/images/generations';
|
|
60
|
+
|
|
61
|
+
// --- prompt construction ---------------------------------------------------
|
|
62
|
+
|
|
63
|
+
// Pull name / role / personality out of an agent profile .md.
|
|
64
|
+
function extractPersona(md, fallbackId) {
|
|
65
|
+
let name = fallbackId;
|
|
66
|
+
let description = '';
|
|
67
|
+
let body = md;
|
|
68
|
+
|
|
69
|
+
if (md.startsWith('---')) {
|
|
70
|
+
const end = md.indexOf('\n---', 3);
|
|
71
|
+
if (end !== -1) {
|
|
72
|
+
const fm = md.slice(3, end);
|
|
73
|
+
const nm = fm.match(/^\s*name:\s*(.+)$/m);
|
|
74
|
+
if (nm) name = nm[1].trim().replace(/^["']|["']$/g, '');
|
|
75
|
+
const dm = fm.match(/^\s*description:\s*(.+)$/m);
|
|
76
|
+
if (dm) description = dm[1].trim().replace(/^["']|["']$/g, '');
|
|
77
|
+
body = md.slice(end + 4);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
82
|
+
const title = titleMatch ? titleMatch[1].trim() : (name.charAt(0).toUpperCase() + name.slice(1));
|
|
83
|
+
|
|
84
|
+
// The Personality section is the richest source of look-and-vibe cues.
|
|
85
|
+
let personality = '';
|
|
86
|
+
const pm = body.match(/^#{2,3}\s*Personality[^\n]*\n([\s\S]*?)(?=\n#{2,3}\s|$)/im);
|
|
87
|
+
if (pm) personality = pm[1].trim();
|
|
88
|
+
if (!personality) {
|
|
89
|
+
// fall back to the opening prose of the profile
|
|
90
|
+
personality = body.replace(/^#.*$/m, '').trim().slice(0, 1200);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { id: name, title, description, personality };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function clip(s, n) {
|
|
97
|
+
s = s.replace(/\s+/g, ' ').trim();
|
|
98
|
+
return s.length > n ? s.slice(0, n).replace(/\s+\S*$/, '') + '…' : s;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildPrompt({ title, description, personality }, appearance) {
|
|
102
|
+
const brief = [
|
|
103
|
+
description && `Role: ${clip(description, 400)}`,
|
|
104
|
+
personality && `Personality & vibe:\n${clip(personality, 1400)}`,
|
|
105
|
+
].filter(Boolean).join('\n\n');
|
|
106
|
+
|
|
107
|
+
const likeness = appearance ? `\nPhysical likeness (use exactly, this overrides any inference): ${appearance}.\n` : '';
|
|
108
|
+
|
|
109
|
+
return `An illustrated character portrait for a software-engineering team roster — a single trading-card-style avatar.
|
|
110
|
+
|
|
111
|
+
Subject: ${title}.
|
|
112
|
+
${likeness}
|
|
113
|
+
${brief}
|
|
114
|
+
|
|
115
|
+
Art direction: a warm, characterful, stylized DIGITAL PORTRAIT ILLUSTRATION (clearly illustrated, NOT photorealistic) of one person who naturally embodies the personality above. Choose an age, build, clothing, expression, and one small thematic prop that fit the role and character. Head and upper chest only, face centered with generous padding on every side so it can be cropped to a circle without clipping. Tasteful thematic gradient background with subtle motifs related to the role. Friendly, professional, a touch playful. Consistent, clean illustrative style. Square 1024x1024.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- generation ------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
function nextVersion(dir, id) {
|
|
121
|
+
if (!fs.existsSync(dir)) return 1;
|
|
122
|
+
const re = new RegExp(`^${id}-(\\d{3})\\.png$`);
|
|
123
|
+
let max = 0;
|
|
124
|
+
for (const f of fs.readdirSync(dir)) {
|
|
125
|
+
const m = f.match(re);
|
|
126
|
+
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
127
|
+
}
|
|
128
|
+
return max + 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function requestBody(prompt) {
|
|
132
|
+
const body = { model: MODEL, prompt, n: 1, size: '1024x1024' };
|
|
133
|
+
const q = process.env.OPENAI_IMAGE_QUALITY;
|
|
134
|
+
if (MODEL === 'dall-e-3') {
|
|
135
|
+
body.quality = q || 'standard'; // standard | hd
|
|
136
|
+
body.response_format = 'b64_json';
|
|
137
|
+
} else {
|
|
138
|
+
body.quality = q || 'high'; // gpt-image-1: low | medium | high | auto
|
|
139
|
+
// gpt-image-1 always returns b64_json; response_format is not accepted.
|
|
140
|
+
}
|
|
141
|
+
return body;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function generate(persona, outDir, version) {
|
|
145
|
+
const v = String(version).padStart(3, '0');
|
|
146
|
+
console.log(` generating ${persona.id} v${v} …`);
|
|
147
|
+
const res = await fetch(API_URL, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${OPENAI_API_KEY}` },
|
|
150
|
+
body: JSON.stringify(requestBody(persona.prompt)),
|
|
151
|
+
});
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
throw new Error(`OpenAI ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
154
|
+
}
|
|
155
|
+
const data = await res.json();
|
|
156
|
+
const d = data.data[0];
|
|
157
|
+
let buf;
|
|
158
|
+
if (d.b64_json) buf = Buffer.from(d.b64_json, 'base64');
|
|
159
|
+
else if (d.url) buf = Buffer.from(await (await fetch(d.url)).arrayBuffer());
|
|
160
|
+
else throw new Error('unexpected response: no b64_json or url');
|
|
161
|
+
|
|
162
|
+
const file = path.join(outDir, `${persona.id}-${v}.png`);
|
|
163
|
+
fs.writeFileSync(file, buf);
|
|
164
|
+
console.log(` ✓ ${path.relative(process.cwd(), file)}`);
|
|
165
|
+
if (d.revised_prompt) console.log(` ↳ revised: ${clip(d.revised_prompt, 90)}`);
|
|
166
|
+
return file;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- main ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
function parseArgs(argv) {
|
|
172
|
+
const opts = { count: 1, out: null, dryRun: false, profiles: [] };
|
|
173
|
+
for (const a of argv) {
|
|
174
|
+
if (a === '--dry-run') opts.dryRun = true;
|
|
175
|
+
else if (a.startsWith('--count=')) opts.count = parseInt(a.split('=')[1], 10);
|
|
176
|
+
else if (a.startsWith('--out=')) opts.out = a.split('=')[1];
|
|
177
|
+
else if (a.startsWith('--appearance=')) opts.appearance = a.slice('--appearance='.length);
|
|
178
|
+
else if (a.startsWith('--prompt-file=')) opts.promptFile = a.slice('--prompt-file='.length);
|
|
179
|
+
else if (a.startsWith('--prompt=')) opts.prompt = a.slice('--prompt='.length);
|
|
180
|
+
else if (a.startsWith('--')) { console.error(`unknown option: ${a}`); process.exit(1); }
|
|
181
|
+
else opts.profiles.push(a);
|
|
182
|
+
}
|
|
183
|
+
return opts;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function main() {
|
|
187
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
188
|
+
|
|
189
|
+
if (!opts.profiles.length) {
|
|
190
|
+
console.error('usage: node lib/generate-team-avatars.mjs <agent.md> [...] [--count=N] [--out=DIR] [--dry-run]');
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
if (!Number.isInteger(opts.count) || opts.count < 1 || opts.count > 5) {
|
|
194
|
+
console.error('--count must be an integer 1-5');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const outDir = path.resolve(opts.out || path.join(REPO_ROOT, 'tmp'));
|
|
199
|
+
|
|
200
|
+
// A verbatim prompt (from --prompt-file or --prompt) overrides the
|
|
201
|
+
// profile-derived prompt entirely — use it for hand-authored / external prompts.
|
|
202
|
+
const rawPrompt = opts.promptFile
|
|
203
|
+
? fs.readFileSync(opts.promptFile, 'utf8').trim()
|
|
204
|
+
: (opts.prompt || null);
|
|
205
|
+
|
|
206
|
+
// Build personas from profiles (profile still supplies the output id/name).
|
|
207
|
+
const personas = opts.profiles.map((p) => {
|
|
208
|
+
const md = fs.readFileSync(p, 'utf8');
|
|
209
|
+
const fallbackId = path.basename(p, '.md');
|
|
210
|
+
const persona = extractPersona(md, fallbackId);
|
|
211
|
+
return { ...persona, prompt: rawPrompt || buildPrompt(persona, opts.appearance) };
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (opts.dryRun) {
|
|
215
|
+
for (const p of personas) {
|
|
216
|
+
console.log(`\n──────── ${p.id} ────────\n${p.prompt}\n`);
|
|
217
|
+
}
|
|
218
|
+
console.log(`(dry run — ${personas.length} profile(s), would generate ${opts.count} each; model ${MODEL})`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!OPENAI_API_KEY) {
|
|
223
|
+
console.error('OPENAI_API_KEY is not set (add it to .env at the repo root).');
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
227
|
+
|
|
228
|
+
const total = personas.length * opts.count;
|
|
229
|
+
console.log(`engsys team avatars — model ${MODEL}, ${opts.count} variation(s) × ${personas.length} profile(s) = ${total} image(s)`);
|
|
230
|
+
console.log(`output: ${outDir}\n`);
|
|
231
|
+
|
|
232
|
+
let ok = 0, fail = 0;
|
|
233
|
+
for (const persona of personas) {
|
|
234
|
+
for (let i = 0; i < opts.count; i++) {
|
|
235
|
+
const version = nextVersion(outDir, persona.id);
|
|
236
|
+
try {
|
|
237
|
+
await generate(persona, outDir, version);
|
|
238
|
+
ok++;
|
|
239
|
+
if (ok + fail < total) await new Promise((r) => setTimeout(r, 1500));
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.error(` ✗ ${persona.id}: ${e.message}`);
|
|
242
|
+
fail++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log(`\ndone — ${ok}/${total} generated${fail ? `, ${fail} failed` : ''}.`);
|
|
248
|
+
console.log('Pick a winner, then move it to team-images/<id>.png.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|
package/lib/manifest.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { path, exists, fs } = require('./util');
|
|
4
|
+
|
|
5
|
+
// Resolve which stack packs a config selects, as [category, value] paths
|
|
6
|
+
// under stacks/. Order matters only for display.
|
|
7
|
+
function selectedPacks(config) {
|
|
8
|
+
const stack = config.stack || {};
|
|
9
|
+
const packs = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
const add = (cat, val) => {
|
|
12
|
+
if (!val || val === 'none') return;
|
|
13
|
+
const rel = path.join(cat, String(val));
|
|
14
|
+
if (seen.has(rel)) return;
|
|
15
|
+
seen.add(rel);
|
|
16
|
+
packs.push(rel);
|
|
17
|
+
};
|
|
18
|
+
// Every stack dimension accepts a scalar OR a list — mixed stacks compose.
|
|
19
|
+
for (const c of [].concat(stack.cloud || [])) add('cloud', c);
|
|
20
|
+
for (const i of [].concat(stack.iac || [])) add('iac', i);
|
|
21
|
+
for (const d of [].concat(stack.db || [])) add('db', d);
|
|
22
|
+
for (const l of [].concat(stack.lang || [])) add('lang', l);
|
|
23
|
+
for (const p of [].concat(stack.platform || [])) add('platform', p);
|
|
24
|
+
for (const d of [].concat(stack.domain || [])) add('domain', d);
|
|
25
|
+
// Issue tracker resolves as a pack too. Defaults to github (zero-config keeps
|
|
26
|
+
// today's behavior); set `issue_tracker: none` to opt out entirely.
|
|
27
|
+
for (const t of [].concat(config.issue_tracker || 'github')) {
|
|
28
|
+
if (t && t !== 'none') add('tooling', `issue-tracker-${t}`);
|
|
29
|
+
}
|
|
30
|
+
return packs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function listMd(dir) {
|
|
34
|
+
if (!exists(dir)) return [];
|
|
35
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build the full install plan: every copy operation + every fragment to merge.
|
|
39
|
+
function buildManifest(engsysRoot, config) {
|
|
40
|
+
const core = path.join(engsysRoot, 'core');
|
|
41
|
+
const plan = {
|
|
42
|
+
agents: [], // { src, name }
|
|
43
|
+
commands: [], // { src, name, description }
|
|
44
|
+
skillDirs: [], // { src(dir), name } -> copied into .claude/skills/<name>
|
|
45
|
+
workflows: [], // { src, name }
|
|
46
|
+
packHooks: [], // { src, name } -> copied verbatim into .claude/hooks/
|
|
47
|
+
claudeFragments: [], // { pack, text }
|
|
48
|
+
settingsFragments: [], // parsed json objects
|
|
49
|
+
mcpServers: {}, // merged
|
|
50
|
+
packs: selectedPacks(config),
|
|
51
|
+
warnings: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const seenAgent = new Set();
|
|
55
|
+
const addAgent = (src, name) => {
|
|
56
|
+
if (seenAgent.has(name)) { plan.warnings.push(`duplicate agent '${name}' skipped (${src})`); return; }
|
|
57
|
+
seenAgent.add(name);
|
|
58
|
+
plan.agents.push({ src, name });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// --- core agents ---
|
|
62
|
+
const agentSel = (config.agents && config.agents.core) || 'all';
|
|
63
|
+
const coreAgentsDir = path.join(core, 'agents');
|
|
64
|
+
const coreAgentFiles = agentSel === 'all'
|
|
65
|
+
? listMd(coreAgentsDir)
|
|
66
|
+
: [].concat(agentSel).map((n) => `${n}.md`);
|
|
67
|
+
for (const f of coreAgentFiles) {
|
|
68
|
+
const src = path.join(coreAgentsDir, f);
|
|
69
|
+
if (!exists(src)) { plan.warnings.push(`core agent not found: ${f}`); continue; }
|
|
70
|
+
addAgent(src, f);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- extra (optional) agents ---
|
|
74
|
+
const extra = (config.agents && config.agents.extra) || [];
|
|
75
|
+
for (const n of [].concat(extra)) {
|
|
76
|
+
const src = path.join(engsysRoot, 'optional-agents', `${n}.md`);
|
|
77
|
+
if (!exists(src)) { plan.warnings.push(`optional agent not found: ${n}`); continue; }
|
|
78
|
+
addAgent(src, `${n}.md`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- commands ---
|
|
82
|
+
const cmdSel = config.commands || 'all';
|
|
83
|
+
const coreCmdDir = path.join(core, 'commands');
|
|
84
|
+
const cmdFiles = cmdSel === 'all'
|
|
85
|
+
? listMd(coreCmdDir)
|
|
86
|
+
: [].concat(cmdSel).map((n) => `${n}.md`);
|
|
87
|
+
const { frontmatterDescription } = require('./util');
|
|
88
|
+
for (const f of cmdFiles) {
|
|
89
|
+
const src = path.join(coreCmdDir, f);
|
|
90
|
+
if (!exists(src)) { plan.warnings.push(`command not found: ${f}`); continue; }
|
|
91
|
+
plan.commands.push({ src, name: f, description: frontmatterDescription(src) });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- core skills (always all) ---
|
|
95
|
+
const coreSkillsDir = path.join(core, 'skills');
|
|
96
|
+
if (exists(coreSkillsDir)) {
|
|
97
|
+
for (const d of fs.readdirSync(coreSkillsDir, { withFileTypes: true })) {
|
|
98
|
+
if (d.isDirectory()) plan.skillDirs.push({ src: path.join(coreSkillsDir, d.name), name: d.name });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- core workflows ---
|
|
103
|
+
const wfDir = path.join(core, 'workflows');
|
|
104
|
+
for (const f of listMd(wfDir)) plan.workflows.push({ src: path.join(wfDir, f), name: f });
|
|
105
|
+
|
|
106
|
+
// --- stack packs ---
|
|
107
|
+
const seenSkill = new Set(plan.skillDirs.map((s) => s.name));
|
|
108
|
+
for (const rel of plan.packs) {
|
|
109
|
+
const packDir = path.join(engsysRoot, 'stacks', rel);
|
|
110
|
+
if (!exists(packDir)) { plan.warnings.push(`pack not found: ${rel}`); continue; }
|
|
111
|
+
|
|
112
|
+
const skillsDir = path.join(packDir, 'skills');
|
|
113
|
+
if (exists(skillsDir)) {
|
|
114
|
+
for (const d of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
115
|
+
if (!d.isDirectory()) continue;
|
|
116
|
+
if (seenSkill.has(d.name)) { plan.warnings.push(`duplicate skill '${d.name}' from ${rel} skipped`); continue; }
|
|
117
|
+
seenSkill.add(d.name);
|
|
118
|
+
plan.skillDirs.push({ src: path.join(skillsDir, d.name), name: d.name });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const agentsDir = path.join(packDir, 'agents');
|
|
123
|
+
for (const f of listMd(agentsDir)) addAgent(path.join(agentsDir, f), f);
|
|
124
|
+
|
|
125
|
+
const hooksDir = path.join(packDir, 'hooks');
|
|
126
|
+
if (exists(hooksDir)) {
|
|
127
|
+
for (const f of fs.readdirSync(hooksDir)) {
|
|
128
|
+
plan.packHooks.push({ src: path.join(hooksDir, f), name: f });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const frag = path.join(packDir, 'claude.fragment.md');
|
|
133
|
+
if (exists(frag)) plan.claudeFragments.push({ pack: rel, text: require('./util').readText(frag).trim() });
|
|
134
|
+
|
|
135
|
+
const sfrag = path.join(packDir, 'settings.fragment.json');
|
|
136
|
+
if (exists(sfrag)) {
|
|
137
|
+
try {
|
|
138
|
+
const obj = JSON.parse(require('./util').readText(sfrag));
|
|
139
|
+
plan.settingsFragments.push(obj);
|
|
140
|
+
Object.assign(plan.mcpServers, obj.mcpServers || {});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
plan.warnings.push(`invalid settings.fragment.json in ${rel}: ${e.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Merge any MCP servers declared directly in naturalize config.
|
|
148
|
+
if (config.naturalize && config.naturalize.mcp_servers) {
|
|
149
|
+
Object.assign(plan.mcpServers, config.naturalize.mcp_servers);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return plan;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { buildManifest, selectedPacks };
|
package/lib/render.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { path, readText, exists, uniq } = require('./util');
|
|
4
|
+
|
|
5
|
+
const PF_START = '<!-- ENGSYS:PROJECT-FACTS:START -->';
|
|
6
|
+
const PF_END = '<!-- ENGSYS:PROJECT-FACTS:END -->';
|
|
7
|
+
|
|
8
|
+
function fill(tmpl, vars) {
|
|
9
|
+
return tmpl.replace(/\{\{(\w+)\}\}/g, (_, k) => (k in vars ? vars[k] : `{{${k}}}`));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function renderCommandsList(commands) {
|
|
13
|
+
if (!commands.length) return '_No commands installed._';
|
|
14
|
+
return commands
|
|
15
|
+
.map((c) => {
|
|
16
|
+
const name = c.name.replace(/\.md$/, '');
|
|
17
|
+
const desc = c.description ? ` — ${c.description}` : '';
|
|
18
|
+
return `- \`/${name}\`${desc}`;
|
|
19
|
+
})
|
|
20
|
+
.join('\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderMcpServers(mcpServers) {
|
|
24
|
+
const keys = Object.keys(mcpServers);
|
|
25
|
+
if (!keys.length) return '_No MCP servers required by the selected stack._';
|
|
26
|
+
return keys
|
|
27
|
+
.map((k) => {
|
|
28
|
+
const s = mcpServers[k];
|
|
29
|
+
const loc = s.url || (s.command ? `${s.command} ${(s.args || []).join(' ')}` : '');
|
|
30
|
+
return `- \`${k}\` — ${s.type || 'stdio'}${loc ? ` (${loc})` : ''}`;
|
|
31
|
+
})
|
|
32
|
+
.join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Render CLAUDE.md from template. On update, preserve the PROJECT-FACTS region
|
|
36
|
+
// from the existing file.
|
|
37
|
+
function renderClaudeMd(engsysRoot, config, plan, existingClaudeMd, seedFacts) {
|
|
38
|
+
const tmpl = readText(path.join(engsysRoot, 'core', 'templates', 'CLAUDE.md.tmpl'));
|
|
39
|
+
const project = config.project || {};
|
|
40
|
+
const nat = config.naturalize || {};
|
|
41
|
+
|
|
42
|
+
const stackFragments = plan.claudeFragments.length
|
|
43
|
+
? plan.claudeFragments.map((f) => f.text).join('\n\n')
|
|
44
|
+
: '## Stack\n\n_No stack packs selected._';
|
|
45
|
+
|
|
46
|
+
let projectFacts = nat.project_facts ||
|
|
47
|
+
'> TODO (naturalize): describe this project — services, runtimes, build/verify toolchain, hard invariants, key paths. Run `/naturalize` or fill this in by hand.';
|
|
48
|
+
|
|
49
|
+
// A foreign CLAUDE.md (or imported AI config) seeds the facts on first adoption.
|
|
50
|
+
if (seedFacts) projectFacts = seedFacts;
|
|
51
|
+
|
|
52
|
+
// Preserve hand-edited project facts on update (prior engsys region wins).
|
|
53
|
+
if (existingClaudeMd) {
|
|
54
|
+
const s = existingClaudeMd.indexOf(PF_START);
|
|
55
|
+
const e = existingClaudeMd.indexOf(PF_END);
|
|
56
|
+
if (s !== -1 && e !== -1 && e > s) {
|
|
57
|
+
const preserved = existingClaudeMd.slice(s + PF_START.length, e).trim();
|
|
58
|
+
if (preserved) projectFacts = preserved;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lessonsCfg = config.lessons || {};
|
|
63
|
+
const lessonsNote = lessonsCfg.seed === false
|
|
64
|
+
? 'Project-specific lessons accrue in `docs/agent-lessons/`.'
|
|
65
|
+
: `Cross-project lessons are seeded under \`${lessonsCfg.into || 'docs/agent-lessons/library'}\` — consult them before implementing. Project-specific lessons accrue in \`docs/agent-lessons/\` and graduate back to the engsys lessons-library by PR.`;
|
|
66
|
+
|
|
67
|
+
return fill(tmpl, {
|
|
68
|
+
PROJECT_NAME: project.name || 'Project',
|
|
69
|
+
PROJECT_DESCRIPTION: project.description || '',
|
|
70
|
+
MODEL_STRATEGY: nat.model_strategy ||
|
|
71
|
+
'Opus for orchestration, synthesis, and judgement; Sonnet for execution. Escalate to Opus when a task needs cross-file reasoning, security analysis, or design tradeoffs.',
|
|
72
|
+
STACK_FRAGMENTS: stackFragments,
|
|
73
|
+
MCP_SERVERS: renderMcpServers(plan.mcpServers),
|
|
74
|
+
COMMANDS_LIST: renderCommandsList(plan.commands),
|
|
75
|
+
PROJECT_FACTS: projectFacts,
|
|
76
|
+
LESSONS_NOTE: lessonsNote,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Merge base settings template with pack permission fragments (+ optional
|
|
81
|
+
// existing settings on update, to preserve hand-added permissions).
|
|
82
|
+
function renderSettings(engsysRoot, plan, existingSettings) {
|
|
83
|
+
const base = JSON.parse(readText(path.join(engsysRoot, 'core', 'templates', 'settings.json.tmpl')));
|
|
84
|
+
base.permissions = base.permissions || { allow: [], deny: [] };
|
|
85
|
+
let allow = base.permissions.allow || [];
|
|
86
|
+
let deny = base.permissions.deny || [];
|
|
87
|
+
|
|
88
|
+
for (const frag of plan.settingsFragments) {
|
|
89
|
+
if (frag.permissions) {
|
|
90
|
+
allow = allow.concat(frag.permissions.allow || []);
|
|
91
|
+
deny = deny.concat(frag.permissions.deny || []);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (existingSettings && existingSettings.permissions) {
|
|
95
|
+
allow = allow.concat(existingSettings.permissions.allow || []);
|
|
96
|
+
deny = deny.concat(existingSettings.permissions.deny || []);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
base.permissions.allow = uniq(allow).sort();
|
|
100
|
+
base.permissions.deny = uniq(deny).sort();
|
|
101
|
+
return JSON.stringify(base, null, 2) + '\n';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderSettingsLocal(engsysRoot, plan) {
|
|
105
|
+
const base = JSON.parse(readText(path.join(engsysRoot, 'core', 'templates', 'settings.local.json.tmpl')));
|
|
106
|
+
base.enabledMcpjsonServers = Object.keys(plan.mcpServers).sort();
|
|
107
|
+
return JSON.stringify(base, null, 2) + '\n';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderMcpJson(plan, existingMcp) {
|
|
111
|
+
const servers = Object.assign({}, (existingMcp && existingMcp.mcpServers) || {}, plan.mcpServers);
|
|
112
|
+
return JSON.stringify({ mcpServers: servers }, null, 2) + '\n';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderHook(engsysRoot, config) {
|
|
116
|
+
const tmpl = readText(path.join(engsysRoot, 'core', 'templates', 'post-edit-reminders.sh.tmpl'));
|
|
117
|
+
const patterns = (config.naturalize && config.naturalize.hook_patterns) || [];
|
|
118
|
+
let cases;
|
|
119
|
+
if (!patterns.length) {
|
|
120
|
+
cases = ' # No project reminders configured yet. Add them under naturalize.hook_patterns.';
|
|
121
|
+
} else {
|
|
122
|
+
cases = patterns
|
|
123
|
+
.map((p) => {
|
|
124
|
+
const reminder = String(p.reminder || '').replace(/"/g, '\\"');
|
|
125
|
+
return ` ${p.glob})\n echo "↳ ${reminder}" ;;`;
|
|
126
|
+
})
|
|
127
|
+
.join('\n');
|
|
128
|
+
}
|
|
129
|
+
return fill(tmpl, { REMINDER_CASES: cases });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
renderClaudeMd, renderSettings, renderSettingsLocal, renderMcpJson, renderHook,
|
|
134
|
+
PF_START, PF_END,
|
|
135
|
+
};
|
package/lib/selftest.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Tiny zero-dep test harness for the YAML subset parser. Run: npm test
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const { parseYaml } = require('./yaml');
|
|
6
|
+
|
|
7
|
+
let pass = 0;
|
|
8
|
+
function check(name, fn) { fn(); pass++; console.log(` ok ${name}`); }
|
|
9
|
+
|
|
10
|
+
check('scalars and inline lists', () => {
|
|
11
|
+
const c = parseYaml(`
|
|
12
|
+
project:
|
|
13
|
+
name: Acme Widgets
|
|
14
|
+
description: A one-liner.
|
|
15
|
+
stack:
|
|
16
|
+
cloud: aws
|
|
17
|
+
iac: none
|
|
18
|
+
lang: [typescript, python]
|
|
19
|
+
db: none
|
|
20
|
+
commands: all
|
|
21
|
+
`);
|
|
22
|
+
assert.strictEqual(c.project.name, 'Acme Widgets');
|
|
23
|
+
assert.strictEqual(c.project.description, 'A one-liner.');
|
|
24
|
+
assert.strictEqual(c.stack.cloud, 'aws');
|
|
25
|
+
assert.deepStrictEqual(c.stack.lang, ['typescript', 'python']);
|
|
26
|
+
assert.strictEqual(c.commands, 'all');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
check('quoted strings keep punctuation', () => {
|
|
30
|
+
const c = parseYaml(`
|
|
31
|
+
naturalize:
|
|
32
|
+
model_strategy: "Opus for judgement; Sonnet for execution."
|
|
33
|
+
`);
|
|
34
|
+
assert.strictEqual(c.naturalize.model_strategy, 'Opus for judgement; Sonnet for execution.');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
check('block list of maps (hook_patterns)', () => {
|
|
38
|
+
const c = parseYaml(`
|
|
39
|
+
naturalize:
|
|
40
|
+
hook_patterns:
|
|
41
|
+
- glob: "*/schema.prisma"
|
|
42
|
+
reminder: "Regenerate the client."
|
|
43
|
+
- glob: "docs/spec.md"
|
|
44
|
+
reminder: "Bump the version header."
|
|
45
|
+
invariants: []
|
|
46
|
+
`);
|
|
47
|
+
assert.strictEqual(c.naturalize.hook_patterns.length, 2);
|
|
48
|
+
assert.strictEqual(c.naturalize.hook_patterns[0].glob, '*/schema.prisma');
|
|
49
|
+
assert.strictEqual(c.naturalize.hook_patterns[0].reminder, 'Regenerate the client.');
|
|
50
|
+
assert.strictEqual(c.naturalize.hook_patterns[1].glob, 'docs/spec.md');
|
|
51
|
+
assert.deepStrictEqual(c.naturalize.invariants, []);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
check('agents block with extra list', () => {
|
|
55
|
+
const c = parseYaml(`
|
|
56
|
+
agents:
|
|
57
|
+
core: all
|
|
58
|
+
extra: [sandy, gary]
|
|
59
|
+
`);
|
|
60
|
+
assert.strictEqual(c.agents.core, 'all');
|
|
61
|
+
assert.deepStrictEqual(c.agents.extra, ['sandy', 'gary']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
check('comments and bools', () => {
|
|
65
|
+
const c = parseYaml(`
|
|
66
|
+
# a comment
|
|
67
|
+
stack:
|
|
68
|
+
cloud: azure # trailing comment
|
|
69
|
+
flag: true
|
|
70
|
+
empty: ~
|
|
71
|
+
`);
|
|
72
|
+
assert.strictEqual(c.stack.cloud, 'azure');
|
|
73
|
+
assert.strictEqual(c.flag, true);
|
|
74
|
+
assert.strictEqual(c.empty, null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
check('inline flow maps (single-line stack/lessons)', () => {
|
|
78
|
+
const c = parseYaml(`
|
|
79
|
+
project: {name: SeedTest, description: x}
|
|
80
|
+
stack: {cloud: aws, lang: [typescript, python], platform: [web]}
|
|
81
|
+
lessons: {seed: false}
|
|
82
|
+
`);
|
|
83
|
+
assert.strictEqual(c.project.name, 'SeedTest');
|
|
84
|
+
assert.strictEqual(c.stack.cloud, 'aws');
|
|
85
|
+
assert.deepStrictEqual(c.stack.lang, ['typescript', 'python']);
|
|
86
|
+
assert.deepStrictEqual(c.stack.platform, ['web']);
|
|
87
|
+
assert.strictEqual(c.lessons.seed, false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log(`\n${pass} checks passed.`);
|