@event4u/agent-config 9.2.0 → 9.4.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.
Files changed (93) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +111 -0
  3. package/CONTRIBUTING.md +14 -0
  4. package/README.md +12 -4
  5. package/dist/agent-src/commands/contribution-precheck.md +100 -0
  6. package/dist/agent-src/commands/fix/route.md +49 -0
  7. package/dist/agent-src/commands/fix.md +4 -2
  8. package/dist/agent-src/commands/tdd/green.md +32 -0
  9. package/dist/agent-src/commands/tdd/red.md +33 -0
  10. package/dist/agent-src/commands/tdd/refactor.md +31 -0
  11. package/dist/agent-src/commands/tdd.md +50 -0
  12. package/dist/agent-src/commands/team.md +4 -0
  13. package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
  14. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +81 -2
  15. package/dist/agent-src/contexts/model-recommendations.md +6 -0
  16. package/dist/agent-src/rules/architecture.md +7 -0
  17. package/dist/agent-src/rules/token-efficiency.md +7 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +5 -0
  19. package/dist/agent-src/skills/ai-council/SKILL.md +3 -0
  20. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -0
  21. package/dist/agent-src/skills/decision-record/SKILL.md +35 -1
  22. package/dist/agent-src/skills/decision-record/evals/triggers.json +82 -12
  23. package/dist/agent-src/skills/decision-record/references/weighted-matrix.md +119 -0
  24. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +142 -0
  25. package/dist/agent-src/skills/fe-design/SKILL.md +1 -0
  26. package/dist/agent-src/skills/judge-code-quality/SKILL.md +1 -0
  27. package/dist/agent-src/skills/judge-synthesis/SKILL.md +1 -0
  28. package/dist/agent-src/skills/learning-tutor/SKILL.md +215 -0
  29. package/dist/agent-src/skills/persona-improvement/SKILL.md +81 -0
  30. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +10 -0
  31. package/dist/agent-src/skills/rice-prioritization/SKILL.md +1 -0
  32. package/dist/agent-src/skills/stakeholder-tradeoff/SKILL.md +3 -1
  33. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +14 -24
  34. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  35. package/dist/discovery/deprecation-report.md +1 -1
  36. package/dist/discovery/discovery-manifest.json +253 -30
  37. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  38. package/dist/discovery/discovery-manifest.summary.md +4 -4
  39. package/dist/discovery/orphan-report.md +1 -1
  40. package/dist/discovery/packs.json +19 -10
  41. package/dist/discovery/trust-report.md +3 -3
  42. package/dist/discovery/workspaces.json +15 -6
  43. package/dist/mcp/registry-manifest.json +3 -3
  44. package/docs/CLAIMS.md +16 -2
  45. package/docs/announcements/2026-07-honest-cost-sweep-launch.md +84 -0
  46. package/docs/anti-reskin-gate.md +60 -0
  47. package/docs/architecture.md +3 -3
  48. package/docs/catalog.md +303 -278
  49. package/docs/command-flows.md +10 -3
  50. package/docs/comparison.yaml +10 -0
  51. package/docs/contracts/adoption-signal-floor.md +17 -0
  52. package/docs/contracts/ai-council-config.md +25 -0
  53. package/docs/contracts/command-clusters.md +2 -1
  54. package/docs/contracts/mcp-tool-inventory.md +25 -22
  55. package/docs/contracts/tier-3-contrib-plugin.md +12 -4
  56. package/docs/decisions/ADR-122-adversarial-verification-council.md +1 -1
  57. package/docs/development.md +10 -1
  58. package/docs/featured-skills.md +2 -2
  59. package/docs/getting-started-by-role.md +1 -1
  60. package/docs/getting-started.md +1 -1
  61. package/docs/governance-advantage.md +1 -1
  62. package/docs/guidelines/component-oriented-and-oop-development.md +181 -0
  63. package/docs/proof.md +10 -6
  64. package/docs/self-review-gate.md +14 -0
  65. package/docs/setup/per-ide/antigravity.md +8 -3
  66. package/docs/us-vs-the-category.md +38 -0
  67. package/docs/wedge/production-validator/README.md +24 -0
  68. package/docs/wedge/production-validator/first-run-check.sh +42 -0
  69. package/package.json +3 -2
  70. package/src/scripts/_lib/catalog_score.ts +64 -0
  71. package/src/scripts/_lib/shingle_similarity.ts +146 -0
  72. package/src/scripts/ai_council/clients.ts +50 -1
  73. package/src/scripts/ai_council/config.ts +28 -3
  74. package/src/scripts/ai_council/low_impact.ts +16 -0
  75. package/src/scripts/ai_council/necessity.ts +11 -0
  76. package/src/scripts/ai_council/orchestrator.ts +78 -26
  77. package/src/scripts/ai_council/pricing.ts +59 -0
  78. package/src/scripts/ai_council/prompts.ts +7 -0
  79. package/src/scripts/ai_council/stance_tally.ts +42 -5
  80. package/src/scripts/build_catalog_index.ts +188 -0
  81. package/src/scripts/council_cli.ts +56 -1
  82. package/src/scripts/generate_index.ts +6 -3
  83. package/src/scripts/hook_manifest.yaml +17 -6
  84. package/src/scripts/install-hooks.sh +32 -17
  85. package/src/scripts/lint_originality.ts +464 -0
  86. package/src/scripts/mcp_server/catalog_tools.ts +114 -0
  87. package/src/scripts/mcp_server/consumer_tool_catalog.json +46 -0
  88. package/src/scripts/pr_url_reminder_hook.ts +177 -0
  89. package/src/scripts/schemas/flow.schema.json +17 -0
  90. package/src/scripts/schemas/skill.schema.json +166 -32
  91. package/src/scripts/self_review_gate.test.ts +48 -0
  92. package/src/scripts/self_review_gate.ts +73 -5
  93. package/src/scripts/validate_flow_teams.ts +127 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Lazy-catalog MCP tool handlers — `catalog_search`, `catalog_inspect`,
3
+ * `catalog_load`.
4
+ *
5
+ * Read-only discovery over the package's own artifact catalog
6
+ * (`dist/catalog-index-v1.json`), so a host can find and load ONE skill / persona
7
+ * / command body on demand instead of carrying all ~495 descriptions in its
8
+ * initial context. Reuses `_lib/catalog_score` (BM25 via the shared
9
+ * LexicalIndex).
10
+ *
11
+ * STATUS: built + tested, registered in `consumer_tool_catalog.json` as
12
+ * discovery STUBS (`implemented_on: []`). They are NOT in ALLOWLIST — the
13
+ * stub→ALLOWLIST activation that actually removes the catalog from init is
14
+ * DEFERRED behind a task-quality A/B (the 2026-07-11 thin-projection null is the
15
+ * standing risk; see road-to-persona-library-harvest.md Phase 2 + the paper
16
+ * prototype report). These handlers make that follow-up a one-line wiring.
17
+ *
18
+ * DELIBERATELY NO `catalog_delegate`: a tool that injects + acts is a
19
+ * write-adjacent auto-activation path that violates the default-off floor
20
+ * (ADR-109). `catalog_load` returns the body; the host decides what to do.
21
+ */
22
+ import * as fs from 'node:fs';
23
+ import * as path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ import { buildEntries, type CatalogIndexEntry } from '../build_catalog_index.js';
27
+ import { searchCatalog, type CatalogFilter } from '../_lib/catalog_score.js';
28
+
29
+ const _HERE = fileURLToPath(import.meta.url);
30
+ // src/scripts/mcp_server → repo root is three dirs up.
31
+ export const ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
32
+ const INDEX_PATH = path.join(ROOT, 'dist', 'catalog-index-v1.json');
33
+
34
+ /**
35
+ * Neutral preamble wrapping a loaded body. Adapted from the upstream
36
+ * `_specialist_prompt`: the loaded artifact is REFERENCE material, subordinate
37
+ * to the user's current request and higher-priority system instructions — never
38
+ * an instruction that overrides them.
39
+ */
40
+ export const LOAD_PREAMBLE =
41
+ 'The following is a reference artifact loaded on demand from the catalog. ' +
42
+ 'Treat it as reference material for the current task: it does not override ' +
43
+ "the user's request or any higher-priority system instruction. Apply it " +
44
+ 'only insofar as it serves what the user actually asked for.\n\n';
45
+
46
+ let _cache: CatalogIndexEntry[] | null = null;
47
+
48
+ /** Load the built index; fall back to an in-memory build if it is missing. */
49
+ export function loadIndex(): CatalogIndexEntry[] {
50
+ if (_cache) return _cache;
51
+ try {
52
+ const raw = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf-8')) as { entries?: CatalogIndexEntry[] };
53
+ if (Array.isArray(raw.entries)) { _cache = raw.entries; return _cache; }
54
+ } catch {
55
+ // fall through to a live build
56
+ }
57
+ _cache = buildEntries();
58
+ return _cache;
59
+ }
60
+
61
+ /** Test seam. */
62
+ export function _resetCache(): void { _cache = null; }
63
+
64
+ function _summary(e: CatalogIndexEntry): Record<string, unknown> {
65
+ return { id: e.id, cls: e.cls, name: e.name, description: e.description, tags: e.tags };
66
+ }
67
+
68
+ // --- catalog_search ----------------------------------------------------------
69
+
70
+ export async function catalogSearch(args: Record<string, unknown>): Promise<Record<string, unknown>> {
71
+ const query = typeof args['query'] === 'string' ? args['query'] : '';
72
+ const cls = args['class'];
73
+ const pack = args['pack'];
74
+ const filter: CatalogFilter = { limit: typeof args['limit'] === 'number' ? args['limit'] : 8 };
75
+ if (cls === 'skill' || cls === 'persona' || cls === 'command') filter.cls = cls;
76
+ if (typeof pack === 'string') filter.pack = pack;
77
+ const hits = searchCatalog(loadIndex(), query, filter);
78
+ return {
79
+ query,
80
+ count: hits.length,
81
+ results: hits.map((h) => ({ ..._summary(h.entry), score: Math.round(h.score * 1000) / 1000 })),
82
+ };
83
+ }
84
+
85
+ // --- catalog_inspect ---------------------------------------------------------
86
+
87
+ export async function catalogInspect(args: Record<string, unknown>): Promise<Record<string, unknown>> {
88
+ const id = typeof args['id'] === 'string' ? args['id'] : '';
89
+ const includeBody = args['include_body'] === true;
90
+ const entry = loadIndex().find((e) => e.id === id);
91
+ if (!entry) return { error: `unknown catalog id: ${id}` };
92
+ const out: Record<string, unknown> = { ..._summary(entry), path: entry.path };
93
+ if (includeBody) out['body'] = _readBody(entry);
94
+ return out;
95
+ }
96
+
97
+ // --- catalog_load ------------------------------------------------------------
98
+
99
+ export async function catalogLoad(args: Record<string, unknown>): Promise<Record<string, unknown>> {
100
+ const id = typeof args['id'] === 'string' ? args['id'] : '';
101
+ const entry = loadIndex().find((e) => e.id === id);
102
+ if (!entry) return { error: `unknown catalog id: ${id}` };
103
+ return { id: entry.id, cls: entry.cls, name: entry.name, path: entry.path, body: LOAD_PREAMBLE + _readBody(entry) };
104
+ }
105
+
106
+ /** Read a body from disk, path-confined to the package root. */
107
+ function _readBody(entry: CatalogIndexEntry): string {
108
+ const abs = path.resolve(ROOT, entry.path);
109
+ // Path-confinement: never read outside the package root, even if the index
110
+ // were tampered with.
111
+ const rootWithSep = ROOT.endsWith(path.sep) ? ROOT : ROOT + path.sep;
112
+ if (!abs.startsWith(rootWithSep)) return '';
113
+ try { return fs.readFileSync(abs, 'utf-8'); } catch { return ''; }
114
+ }
@@ -351,6 +351,52 @@
351
351
  "additionalProperties": false
352
352
  }
353
353
  },
354
+ {
355
+ "name": "catalog_search",
356
+ "description": "Search the package's own artifact catalog (skills, personas, commands) by free-text query and return ranked summaries — name, description, tags, score — WITHOUT loading any body. Use to discover which skill or persona fits a task when the full catalog is not in context. Read-only, deterministic (BM25 over an on-disk index). DISCOVERY STUB — the real handler ships behind a task-quality A/B before activation (see road-to-persona-library-harvest.md Phase 2); until then this returns the not_implemented envelope.",
357
+ "side_effect": "ro",
358
+ "implemented_on": [],
359
+ "input_schema": {
360
+ "type": "object",
361
+ "properties": {
362
+ "query": {"type": "string", "description": "Free-text search query."},
363
+ "class": {"type": "string", "enum": ["skill", "persona", "command"], "description": "Optional: restrict to one artifact class."},
364
+ "pack": {"type": "string", "description": "Optional: restrict to entries tagged with this pack/domain."},
365
+ "limit": {"type": "integer", "minimum": 1, "default": 8, "description": "Maximum results. Defaults to 8."}
366
+ },
367
+ "required": ["query"],
368
+ "additionalProperties": false
369
+ }
370
+ },
371
+ {
372
+ "name": "catalog_inspect",
373
+ "description": "Return the metadata for one catalog entry by id (id, class, name, description, tags, path). Body is returned ONLY when include_body is true. Use after catalog_search to look at a candidate before loading it. Read-only. DISCOVERY STUB — real handler activation deferred (see catalog_search).",
374
+ "side_effect": "ro",
375
+ "implemented_on": [],
376
+ "input_schema": {
377
+ "type": "object",
378
+ "properties": {
379
+ "id": {"type": "string", "description": "Catalog id, e.g. `skill:api-design` or `persona:backend-architect`."},
380
+ "include_body": {"type": "boolean", "default": false, "description": "When true, include the full body. Defaults to false (metadata only)."}
381
+ },
382
+ "required": ["id"],
383
+ "additionalProperties": false
384
+ }
385
+ },
386
+ {
387
+ "name": "catalog_load",
388
+ "description": "Load the full body of one catalog entry by id, wrapped in a neutral reference preamble that subordinates it to the user's request and system instructions. Use to bring a skill/persona/command body into context on demand. Read-only, path-confined to the package root. Never injects or acts — the host decides what to do with the body. DISCOVERY STUB — real handler activation deferred (see catalog_search).",
389
+ "side_effect": "ro",
390
+ "implemented_on": [],
391
+ "input_schema": {
392
+ "type": "object",
393
+ "properties": {
394
+ "id": {"type": "string", "description": "Catalog id to load the body for."}
395
+ },
396
+ "required": ["id"],
397
+ "additionalProperties": false
398
+ }
399
+ },
354
400
  {
355
401
  "name": "council_estimate",
356
402
  "description": "Estimate the token cost of an AI-council debate over a given input (roadmap, diff, prompt, or file set) without spending — no network call, no billing. Use before deciding whether to authorize a real council run. Read-only.",
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse PR-URL reminder — reply-shape backstop for `direct-answers`.
4
+ *
5
+ * The `direct-answers` rule (reply-close) requires: a PR created THIS turn puts
6
+ * its raw URL as the **literal last line** of the reply. That is a behavioural
7
+ * rule with no deterministic backstop, so it can be silently missed — the URL
8
+ * ends up inline mid-reply instead of last. This hook fires right after a
9
+ * PR-creating tool call, extracts the created PR URL from the tool output, and
10
+ * WARNS in context (exit 2) so the reminder lands exactly when the PR is born,
11
+ * before the reply closes.
12
+ *
13
+ * Fires ONLY on an actual PR *create*:
14
+ * - a shell command matching `gh pr create`, OR
15
+ * - a GitHub tool whose name means "create a pull request"
16
+ * (e.g. create_pull_request / createPullRequest / pulls.create),
17
+ * AND only when the tool output actually contains a `…/pull/<n>` URL — so a
18
+ * failed / dry-run create (no URL) does not nag, and `gh pr view/checks/list/
19
+ * comment` (which also print PR URLs) never fire it.
20
+ *
21
+ * Warn only — never blocks (fail_closed: false). Unconditional: it reinforces
22
+ * an always-on rule and only fires on the rare PR-create event, so it carries
23
+ * no settings gate (mirrors verify-before-complete / roadmap-progress).
24
+ *
25
+ * Exit codes (dispatcher contract): 0 allow · 2 warn (+ `{decision,reason}`
26
+ * JSON on stdout).
27
+ */
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import { fileURLToPath, pathToFileURL } from "node:url";
31
+
32
+ const EXIT_ALLOW = 0;
33
+ const EXIT_WARN = 2;
34
+
35
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
36
+ type JsonObject = { [k: string]: JsonValue };
37
+
38
+ // Tool names across platforms whose command field carries a shell command
39
+ // (kept in lock-step with verify_before_complete_hook.COMMAND_TOOLS).
40
+ const COMMAND_TOOLS: ReadonlySet<string> = new Set([
41
+ "launch-process",
42
+ "launch_process",
43
+ "Bash",
44
+ "BashTool",
45
+ "run-process",
46
+ "runProcess",
47
+ "shell",
48
+ "execute_shell",
49
+ "RunShellCommand",
50
+ ]);
51
+
52
+ // A shell command that CREATES a pull request. Anchored on a shell separator so
53
+ // it matches inside a chained command (`git push && gh pr create …`) but not a
54
+ // substring of another word. `gh pr view/checks/list/comment` do not match.
55
+ const _GH_PR_CREATE = /(?:^|[\s;&|`(])gh\s+pr\s+create\b/i;
56
+
57
+ // A non-shell tool whose NAME means "create a pull request" (github-api / MCP).
58
+ const _CREATE_PR_TOOL = /(?:create[_-]?pull_?request|createpullrequest|pulls[._-]create|create_pr\b)/i;
59
+
60
+ // A GitHub pull-request URL as printed by `gh pr create` / returned as html_url.
61
+ const _PR_URL = /https?:\/\/github\.com\/[^\s"'`<>]+\/pull\/\d+/i;
62
+
63
+ function _isObject(v: unknown): v is JsonObject {
64
+ return typeof v === "object" && v !== null && !Array.isArray(v);
65
+ }
66
+
67
+ /** Return [tool_name, command_text] from a tool-event payload. */
68
+ function _extractCommand(payload: JsonObject): [string | null, string | null] {
69
+ const toolRaw = payload["tool_name"] ?? payload["toolName"] ?? payload["tool"];
70
+ const tool = typeof toolRaw === "string" ? toolRaw : null;
71
+ if (!(tool !== null && COMMAND_TOOLS.has(tool))) {
72
+ return [tool, null];
73
+ }
74
+ const ti = payload["tool_input"];
75
+ if (_isObject(ti)) {
76
+ for (const key of ["command", "cmd", "shell_command"]) {
77
+ const v = ti[key];
78
+ if (typeof v === "string" && v) {
79
+ return [tool, v];
80
+ }
81
+ }
82
+ }
83
+ for (const key of ["command", "cmd"]) {
84
+ const v = payload[key];
85
+ if (typeof v === "string" && v) {
86
+ return [tool, v];
87
+ }
88
+ }
89
+ return [tool, null];
90
+ }
91
+
92
+ /** Best-effort extraction of the tool-output text from the envelope. */
93
+ function _toolOutput(envelope: JsonObject): string {
94
+ for (const key of ["tool_response", "tool_result", "toolResponse", "output", "result", "stdout"]) {
95
+ const v = envelope[key];
96
+ if (typeof v === "string") {
97
+ return v;
98
+ }
99
+ if (_isObject(v) || Array.isArray(v)) {
100
+ return JSON.stringify(v);
101
+ }
102
+ }
103
+ return JSON.stringify(envelope);
104
+ }
105
+
106
+ /** True when this tool event created a pull request. */
107
+ function _isPrCreate(tool: string | null, command: string | null): boolean {
108
+ if (command !== null && _GH_PR_CREATE.test(command)) {
109
+ return true;
110
+ }
111
+ return tool !== null && _CREATE_PR_TOOL.test(tool);
112
+ }
113
+
114
+ export function main(): number {
115
+ let envelope: JsonValue;
116
+ try {
117
+ const raw = _readStdin();
118
+ envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
119
+ } catch {
120
+ return EXIT_ALLOW; // never block on a malformed envelope
121
+ }
122
+ if (!_isObject(envelope)) {
123
+ return EXIT_ALLOW;
124
+ }
125
+
126
+ const [tool, command] = _extractCommand(envelope);
127
+ if (!_isPrCreate(tool, command)) {
128
+ return EXIT_ALLOW;
129
+ }
130
+
131
+ const match = _PR_URL.exec(_toolOutput(envelope));
132
+ if (match === null) {
133
+ return EXIT_ALLOW; // create attempted but no URL produced (failed / dry-run) — do not nag
134
+ }
135
+ const url = match[0];
136
+
137
+ // `reason` (≤ 200 chars) goes to stderr; `additional_context` is what
138
+ // surfaces back to the model on platforms that support it — so the
139
+ // actionable instruction lives there (a stderr-only reason would not
140
+ // change the reply that this hook exists to shape).
141
+ const reason = `PR created this turn (${url}) — end your reply with this raw URL as the literal last line (direct-answers/reply-close).`;
142
+ const additional_context =
143
+ `You created a pull request this turn: ${url}\n\n` +
144
+ `Per the direct-answers rule (reply-close): a PR created this turn must appear ` +
145
+ `as the raw URL on the LITERAL LAST LINE of your reply — not inline mid-message. ` +
146
+ `Before ending the turn, make your final line exactly:\n${url}\n` +
147
+ `If it already is, no action needed.`;
148
+ process.stdout.write(`${JSON.stringify({ decision: "warn", reason, additional_context })}\n`);
149
+ return EXIT_WARN;
150
+ }
151
+
152
+ function _readStdin(): string {
153
+ return fs.readFileSync(0, "utf-8");
154
+ }
155
+
156
+ function _isCliEntry(): boolean {
157
+ if (process.argv[1] === undefined) {
158
+ return false;
159
+ }
160
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
161
+ if (import.meta.url === argvUrl) {
162
+ return true;
163
+ }
164
+ // Symlinked invocation (installed projection, or macOS /var → /private/var
165
+ // temp dirs) makes the raw URLs differ; compare realpaths so the guard fires.
166
+ try {
167
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
168
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
169
+ return here === argv;
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ if (_isCliEntry()) {
176
+ process.exit(main());
177
+ }
@@ -47,6 +47,23 @@
47
47
  "type": "array",
48
48
  "items": { "$ref": "#/definitions/commandRef" },
49
49
  "description": "Optional descriptive seed list lifted from the command-classification worksheet `·_flow:` tags. Annotation only — entry_points / default_path are the validated reference fields. Each entry, if present, must still resolve to a real command."
50
+ },
51
+ "team": {
52
+ "type": "object",
53
+ "additionalProperties": false,
54
+ "description": "OPTIONAL demand-probe annotation (road-to-persona-library-harvest.md Phase 3): the persona/skill team that this flow's work typically pulls in. Annotation ONLY — no execution semantics; subagent-orchestration remains the executor. Referential integrity (every persona id / skill slug resolves) is checked by scripts/validate_flow_teams.ts, not this schema. Exists so demand for scenario→team selection can reveal itself before a roster schema is designed.",
55
+ "properties": {
56
+ "personas": {
57
+ "type": "array",
58
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
59
+ "description": "Persona ids the flow typically pulls in. Each must resolve to a real src/agent-src/personas/<id>.md."
60
+ },
61
+ "skills": {
62
+ "type": "array",
63
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
64
+ "description": "Skill slugs the flow's team relies on. Each must resolve to a real skills/<slug>/SKILL.md."
65
+ }
66
+ }
50
67
  }
51
68
  },
52
69
  "definitions": {