@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +111 -0
- package/CONTRIBUTING.md +14 -0
- package/README.md +12 -4
- package/dist/agent-src/commands/contribution-precheck.md +100 -0
- package/dist/agent-src/commands/fix/route.md +49 -0
- package/dist/agent-src/commands/fix.md +4 -2
- package/dist/agent-src/commands/tdd/green.md +32 -0
- package/dist/agent-src/commands/tdd/red.md +33 -0
- package/dist/agent-src/commands/tdd/refactor.md +31 -0
- package/dist/agent-src/commands/tdd.md +50 -0
- package/dist/agent-src/commands/team.md +4 -0
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +81 -2
- package/dist/agent-src/contexts/model-recommendations.md +6 -0
- package/dist/agent-src/rules/architecture.md +7 -0
- package/dist/agent-src/rules/token-efficiency.md +7 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +5 -0
- package/dist/agent-src/skills/ai-council/SKILL.md +3 -0
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -0
- package/dist/agent-src/skills/decision-record/SKILL.md +35 -1
- package/dist/agent-src/skills/decision-record/evals/triggers.json +82 -12
- package/dist/agent-src/skills/decision-record/references/weighted-matrix.md +119 -0
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +142 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -0
- package/dist/agent-src/skills/judge-code-quality/SKILL.md +1 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +1 -0
- package/dist/agent-src/skills/learning-tutor/SKILL.md +215 -0
- package/dist/agent-src/skills/persona-improvement/SKILL.md +81 -0
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +10 -0
- package/dist/agent-src/skills/rice-prioritization/SKILL.md +1 -0
- package/dist/agent-src/skills/stakeholder-tradeoff/SKILL.md +3 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +14 -24
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +253 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +19 -10
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +15 -6
- package/dist/mcp/registry-manifest.json +3 -3
- package/docs/CLAIMS.md +16 -2
- package/docs/announcements/2026-07-honest-cost-sweep-launch.md +84 -0
- package/docs/anti-reskin-gate.md +60 -0
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +303 -278
- package/docs/command-flows.md +10 -3
- package/docs/comparison.yaml +10 -0
- package/docs/contracts/adoption-signal-floor.md +17 -0
- package/docs/contracts/ai-council-config.md +25 -0
- package/docs/contracts/command-clusters.md +2 -1
- package/docs/contracts/mcp-tool-inventory.md +25 -22
- package/docs/contracts/tier-3-contrib-plugin.md +12 -4
- package/docs/decisions/ADR-122-adversarial-verification-council.md +1 -1
- package/docs/development.md +10 -1
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/component-oriented-and-oop-development.md +181 -0
- package/docs/proof.md +10 -6
- package/docs/self-review-gate.md +14 -0
- package/docs/setup/per-ide/antigravity.md +8 -3
- package/docs/us-vs-the-category.md +38 -0
- package/docs/wedge/production-validator/README.md +24 -0
- package/docs/wedge/production-validator/first-run-check.sh +42 -0
- package/package.json +3 -2
- package/src/scripts/_lib/catalog_score.ts +64 -0
- package/src/scripts/_lib/shingle_similarity.ts +146 -0
- package/src/scripts/ai_council/clients.ts +50 -1
- package/src/scripts/ai_council/config.ts +28 -3
- package/src/scripts/ai_council/low_impact.ts +16 -0
- package/src/scripts/ai_council/necessity.ts +11 -0
- package/src/scripts/ai_council/orchestrator.ts +78 -26
- package/src/scripts/ai_council/pricing.ts +59 -0
- package/src/scripts/ai_council/prompts.ts +7 -0
- package/src/scripts/ai_council/stance_tally.ts +42 -5
- package/src/scripts/build_catalog_index.ts +188 -0
- package/src/scripts/council_cli.ts +56 -1
- package/src/scripts/generate_index.ts +6 -3
- package/src/scripts/hook_manifest.yaml +17 -6
- package/src/scripts/install-hooks.sh +32 -17
- package/src/scripts/lint_originality.ts +464 -0
- package/src/scripts/mcp_server/catalog_tools.ts +114 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +46 -0
- package/src/scripts/pr_url_reminder_hook.ts +177 -0
- package/src/scripts/schemas/flow.schema.json +17 -0
- package/src/scripts/schemas/skill.schema.json +166 -32
- package/src/scripts/self_review_gate.test.ts +48 -0
- package/src/scripts/self_review_gate.ts +73 -5
- package/src/scripts/validate_flow_teams.ts +127 -0
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
"$comment": "Source: agents/reference/docs/frontmatter-contract.md#skills. Keep in sync with inventory_frontmatter.py and the linter's lint_skill().",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
9
|
-
"required": [
|
|
9
|
+
"required": [
|
|
10
|
+
"name",
|
|
11
|
+
"description",
|
|
12
|
+
"source",
|
|
13
|
+
"domain"
|
|
14
|
+
],
|
|
10
15
|
"properties": {
|
|
11
16
|
"name": {
|
|
12
17
|
"type": "string",
|
|
@@ -28,19 +33,41 @@
|
|
|
28
33
|
"maxLength": 500,
|
|
29
34
|
"description": "Agent-Skills spec field (optional): machine-readable declaration of environment requirements — system packages, network access, product/host constraints. Mirrors the public spec so media/doc skills can state requirements outside prose."
|
|
30
35
|
},
|
|
36
|
+
"provenance": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": [
|
|
39
|
+
"official",
|
|
40
|
+
"community"
|
|
41
|
+
],
|
|
42
|
+
"description": "Catalog source attribution: absent or 'official' = maintainer-authored; 'community' = merged external contribution (credited in CREDITS.md; rendered in the catalog's source column)."
|
|
43
|
+
},
|
|
31
44
|
"source": {
|
|
32
45
|
"type": "string",
|
|
33
|
-
"enum": [
|
|
46
|
+
"enum": [
|
|
47
|
+
"package",
|
|
48
|
+
"project"
|
|
49
|
+
],
|
|
34
50
|
"default": "package"
|
|
35
51
|
},
|
|
36
52
|
"domain": {
|
|
37
53
|
"type": "string",
|
|
38
|
-
"enum": [
|
|
54
|
+
"enum": [
|
|
55
|
+
"engineering",
|
|
56
|
+
"product",
|
|
57
|
+
"quality",
|
|
58
|
+
"devops",
|
|
59
|
+
"process",
|
|
60
|
+
"discovery"
|
|
61
|
+
],
|
|
39
62
|
"description": "Skill classification axis; see docs/contracts/skill-domains.md. Required for every skill — exactly one of: engineering, product, quality, devops, process, discovery."
|
|
40
63
|
},
|
|
41
64
|
"status": {
|
|
42
65
|
"type": "string",
|
|
43
|
-
"enum": [
|
|
66
|
+
"enum": [
|
|
67
|
+
"active",
|
|
68
|
+
"deprecated",
|
|
69
|
+
"superseded"
|
|
70
|
+
]
|
|
44
71
|
},
|
|
45
72
|
"replaced_by": {
|
|
46
73
|
"type": "string",
|
|
@@ -60,7 +87,9 @@
|
|
|
60
87
|
},
|
|
61
88
|
"tier": {
|
|
62
89
|
"type": "string",
|
|
63
|
-
"enum": [
|
|
90
|
+
"enum": [
|
|
91
|
+
"senior"
|
|
92
|
+
],
|
|
64
93
|
"description": "Optional tier marker. `senior` opts the skill into the Senior-Tier Required Structure check (Context-First lead, Related Skills, Proactive Triggers, Output Artifacts) per .agent-src.uncondensed/rules/skill-quality.md."
|
|
65
94
|
},
|
|
66
95
|
"meta_skill": {
|
|
@@ -84,23 +113,37 @@
|
|
|
84
113
|
},
|
|
85
114
|
"model_tier": {
|
|
86
115
|
"type": "string",
|
|
87
|
-
"enum": [
|
|
116
|
+
"enum": [
|
|
117
|
+
"lite",
|
|
118
|
+
"medium",
|
|
119
|
+
"high",
|
|
120
|
+
"inherit"
|
|
121
|
+
],
|
|
88
122
|
"description": "Vendor-neutral capability band the skill needs (ADR-035). high/medium/lite project to a native Claude `model:` (high→opus, medium→sonnet, lite→haiku) when model.auto_switch=auto; the generator owns that mapping exclusively. inherit keeps the session model. Non-Claude agents are suggestion-only — the model-recommendation rule names the tier and the agent resolves its own model. No vendor model names here."
|
|
89
123
|
},
|
|
90
124
|
"context": {
|
|
91
125
|
"type": "string",
|
|
92
|
-
"enum": [
|
|
126
|
+
"enum": [
|
|
127
|
+
"large"
|
|
128
|
+
],
|
|
93
129
|
"description": "Optional orthogonal modifier (ADR-035): the skill needs a large-context model regardless of reasoning tier (e.g. summarising a 500-page log). Sparse — most skills omit it. Metadata only; does not change the native Claude `model:` (the tier owns the model choice)."
|
|
94
130
|
},
|
|
95
131
|
"effort": {
|
|
96
132
|
"type": "string",
|
|
97
|
-
"enum": [
|
|
133
|
+
"enum": [
|
|
134
|
+
"low",
|
|
135
|
+
"medium",
|
|
136
|
+
"high",
|
|
137
|
+
"max"
|
|
138
|
+
],
|
|
98
139
|
"description": "Optional pinned reasoning-effort level for hosts with an effort knob (Claude Code `--effort` / agent frontmatter). Passthrough: skills project verbatim, so the key reaches Claude Code as-is and is inert on hosts without the knob. The bench harness pins it per run (bench_ab_task_runner `--effort`) so measurements are reproducible. Orthogonal to `model_tier` (capability band) — effort tunes how hard the model thinks, not which model runs. Omit unless the skill measurably needs a non-default effort.",
|
|
99
140
|
"$comment": "road-to-ecosystem-harvest-skill-quality-gates Phase 4, Source S (pinned effort frontmatter)."
|
|
100
141
|
},
|
|
101
142
|
"council_depth": {
|
|
102
143
|
"type": "string",
|
|
103
|
-
"enum": [
|
|
144
|
+
"enum": [
|
|
145
|
+
"deep"
|
|
146
|
+
],
|
|
104
147
|
"description": "Optional reasoning-depth marker for AI Council invocations triggered by this skill. The only accepted value is 'deep'; omit the key for default depth (setting 'standard' is rejected — every frontmatter byte counts against the context window, and 'standard' is the implicit default). 'deep' instructs the host agent to pass --depth deep to council_cli, which floors rounds at max(ai_council.deep_min_rounds, ai_council.min_rounds). Use for architecture, refactoring, or bug-diagnosis skills. See .agent-src.uncondensed/skills/ai-council/SKILL.md."
|
|
105
148
|
},
|
|
106
149
|
"context_spine": {
|
|
@@ -108,28 +151,52 @@
|
|
|
108
151
|
"uniqueItems": true,
|
|
109
152
|
"items": {
|
|
110
153
|
"type": "string",
|
|
111
|
-
"enum": [
|
|
154
|
+
"enum": [
|
|
155
|
+
"product",
|
|
156
|
+
"team",
|
|
157
|
+
"repo",
|
|
158
|
+
"channel-stage",
|
|
159
|
+
"funnel-stage",
|
|
160
|
+
"customer-segment",
|
|
161
|
+
"fiscal-period",
|
|
162
|
+
"org-stage",
|
|
163
|
+
"regulatory-regime"
|
|
164
|
+
]
|
|
112
165
|
},
|
|
113
166
|
"description": "Senior-skill opt-in for the context spine. Declares which slots under agents/context-spine/ the skill expects to read. Cross-wing slots (product, team, repo) are locked at 3 by council Q1 (KEEP-3); wing-scoped slots follow the per-wing ADR track in docs/contracts/context-spine.md § 5. Wing-3 (channel-stage, funnel-stage, customer-segment) authorized by docs/contracts/adr-gtm-context-spine.md; Wing-4 (fiscal-period, org-stage, regulatory-regime) authorized by docs/contracts/adr-wing4-context-spine.md."
|
|
114
167
|
},
|
|
115
168
|
"recommended_for_user_types": {
|
|
116
169
|
"type": "array",
|
|
117
170
|
"uniqueItems": true,
|
|
118
|
-
"items": {
|
|
171
|
+
"items": {
|
|
172
|
+
"type": "string"
|
|
173
|
+
},
|
|
119
174
|
"description": "Forward-compatible user-type recommendation tags (step-12-universal-os-reframe Phase 5). Skill loads for every user-type whose slug appears in this list. Absence of the key marks the skill as universal — see docs/contracts/universal-skills.md for the always-loaded floor and docs/contracts/router-blending.md for per-user-type mix ratios. Wired by step-9-user-types-axis once it lands; treated as metadata until then. Free-form tags (e.g. 'creator', 'founder', 'consultant', 'gtm', 'finance', 'ops', 'developer') — no enum until the user-types axis closes."
|
|
120
175
|
},
|
|
121
176
|
"execution": {
|
|
122
177
|
"type": "object",
|
|
123
178
|
"additionalProperties": false,
|
|
124
|
-
"required": [
|
|
179
|
+
"required": [
|
|
180
|
+
"type"
|
|
181
|
+
],
|
|
125
182
|
"properties": {
|
|
126
183
|
"type": {
|
|
127
184
|
"type": "string",
|
|
128
|
-
"enum": [
|
|
185
|
+
"enum": [
|
|
186
|
+
"manual",
|
|
187
|
+
"assisted",
|
|
188
|
+
"automated"
|
|
189
|
+
]
|
|
129
190
|
},
|
|
130
191
|
"handler": {
|
|
131
192
|
"type": "string",
|
|
132
|
-
"enum": [
|
|
193
|
+
"enum": [
|
|
194
|
+
"none",
|
|
195
|
+
"shell",
|
|
196
|
+
"php",
|
|
197
|
+
"node",
|
|
198
|
+
"internal"
|
|
199
|
+
]
|
|
133
200
|
},
|
|
134
201
|
"timeout_seconds": {
|
|
135
202
|
"type": "integer",
|
|
@@ -137,22 +204,30 @@
|
|
|
137
204
|
},
|
|
138
205
|
"safety_mode": {
|
|
139
206
|
"type": "string",
|
|
140
|
-
"enum": [
|
|
207
|
+
"enum": [
|
|
208
|
+
"strict"
|
|
209
|
+
]
|
|
141
210
|
},
|
|
142
211
|
"allowed_tools": {
|
|
143
212
|
"type": "array",
|
|
144
|
-
"items": {
|
|
213
|
+
"items": {
|
|
214
|
+
"type": "string"
|
|
215
|
+
},
|
|
145
216
|
"description": "Allow-list of tool grants. Prefer the scoped-grant syntax over bare tool names where the host supports it — e.g. Bash(npm test:*) instead of Bash — per tool-safety Least Agency."
|
|
146
217
|
},
|
|
147
218
|
"disallowed_tools": {
|
|
148
219
|
"type": "array",
|
|
149
|
-
"items": {
|
|
220
|
+
"items": {
|
|
221
|
+
"type": "string"
|
|
222
|
+
},
|
|
150
223
|
"description": "U3 (ecosystem harvest): optional deny-list layered UNDER allowed_tools — defense-in-depth; a tool matching here is refused even if a broad allow pattern would admit it. Supports the scoped-grant syntax (e.g. Bash(git push:*))."
|
|
151
224
|
},
|
|
152
225
|
"command": {
|
|
153
226
|
"type": "array",
|
|
154
227
|
"minItems": 1,
|
|
155
|
-
"items": {
|
|
228
|
+
"items": {
|
|
229
|
+
"type": "string"
|
|
230
|
+
}
|
|
156
231
|
}
|
|
157
232
|
}
|
|
158
233
|
},
|
|
@@ -160,19 +235,30 @@
|
|
|
160
235
|
"type": "array",
|
|
161
236
|
"minItems": 1,
|
|
162
237
|
"uniqueItems": true,
|
|
163
|
-
"items": {
|
|
238
|
+
"items": {
|
|
239
|
+
"type": "string",
|
|
240
|
+
"pattern": "^[a-z][a-z0-9-]*$"
|
|
241
|
+
},
|
|
164
242
|
"description": "ADR-013 workspace ids (additive discovery axis). Closed vocabulary enforced by scripts/lint_artefact_frontmatter.py against config/discovery/workspaces.yml."
|
|
165
243
|
},
|
|
166
244
|
"packs": {
|
|
167
245
|
"type": "array",
|
|
168
246
|
"minItems": 1,
|
|
169
247
|
"uniqueItems": true,
|
|
170
|
-
"items": {
|
|
248
|
+
"items": {
|
|
249
|
+
"type": "string",
|
|
250
|
+
"pattern": "^[a-z][a-z0-9-]*$"
|
|
251
|
+
},
|
|
171
252
|
"description": "ADR-013 pack ids (additive discovery axis). Closed vocabulary enforced by scripts/lint_artefact_frontmatter.py against config/discovery/packs.yml."
|
|
172
253
|
},
|
|
173
254
|
"lifecycle": {
|
|
174
255
|
"type": "string",
|
|
175
|
-
"enum": [
|
|
256
|
+
"enum": [
|
|
257
|
+
"active",
|
|
258
|
+
"deprecated",
|
|
259
|
+
"experimental",
|
|
260
|
+
"archived"
|
|
261
|
+
],
|
|
176
262
|
"default": "active",
|
|
177
263
|
"description": "ADR-013 lifecycle state."
|
|
178
264
|
},
|
|
@@ -180,38 +266,83 @@
|
|
|
180
266
|
"type": "array",
|
|
181
267
|
"minItems": 1,
|
|
182
268
|
"uniqueItems": true,
|
|
183
|
-
"items": {
|
|
269
|
+
"items": {
|
|
270
|
+
"type": "string",
|
|
271
|
+
"pattern": "^[a-z][a-z0-9-]*$"
|
|
272
|
+
},
|
|
184
273
|
"description": "Skill-composition graph (roadmap 3.4): names of sub-skills this skill's body invokes/assumes. Distinct from ADR-015 `requires` (artefact→pack edges). scripts/check_skill_requires.py enforces (a) each target exists and (b) the required skill is co-available wherever this skill ships (same pack, a requires_hint-reachable pack, or always-on)."
|
|
185
274
|
},
|
|
186
275
|
"trust": {
|
|
187
276
|
"type": "object",
|
|
188
277
|
"additionalProperties": false,
|
|
189
|
-
"required": [
|
|
278
|
+
"required": [
|
|
279
|
+
"level",
|
|
280
|
+
"confidence",
|
|
281
|
+
"human_review_required"
|
|
282
|
+
],
|
|
190
283
|
"properties": {
|
|
191
|
-
"level": {
|
|
192
|
-
|
|
193
|
-
|
|
284
|
+
"level": {
|
|
285
|
+
"type": "string",
|
|
286
|
+
"enum": [
|
|
287
|
+
"core",
|
|
288
|
+
"professional",
|
|
289
|
+
"experimental",
|
|
290
|
+
"advisory",
|
|
291
|
+
"restricted"
|
|
292
|
+
],
|
|
293
|
+
"default": "core"
|
|
294
|
+
},
|
|
295
|
+
"confidence": {
|
|
296
|
+
"type": "string",
|
|
297
|
+
"enum": [
|
|
298
|
+
"high",
|
|
299
|
+
"medium",
|
|
300
|
+
"low"
|
|
301
|
+
],
|
|
302
|
+
"default": "high"
|
|
303
|
+
},
|
|
304
|
+
"human_review_required": {
|
|
305
|
+
"type": "boolean",
|
|
306
|
+
"default": false
|
|
307
|
+
}
|
|
194
308
|
},
|
|
195
309
|
"description": "ADR-013 trust block. Vocabulary mirrored by scripts/lint_artefact_frontmatter.py."
|
|
196
310
|
},
|
|
197
311
|
"install": {
|
|
198
312
|
"type": "object",
|
|
199
313
|
"additionalProperties": false,
|
|
200
|
-
"required": [
|
|
314
|
+
"required": [
|
|
315
|
+
"default",
|
|
316
|
+
"removable"
|
|
317
|
+
],
|
|
201
318
|
"properties": {
|
|
202
|
-
"default": {
|
|
203
|
-
|
|
319
|
+
"default": {
|
|
320
|
+
"type": "boolean",
|
|
321
|
+
"default": true
|
|
322
|
+
},
|
|
323
|
+
"removable": {
|
|
324
|
+
"type": "boolean",
|
|
325
|
+
"default": false
|
|
326
|
+
}
|
|
204
327
|
},
|
|
205
328
|
"description": "ADR-013 install hints (default-on at consumer install; user-removable)."
|
|
206
329
|
},
|
|
207
330
|
"token_budget_class": {
|
|
208
331
|
"type": "string",
|
|
209
|
-
"enum": [
|
|
332
|
+
"enum": [
|
|
333
|
+
"lean",
|
|
334
|
+
"standard",
|
|
335
|
+
"rich"
|
|
336
|
+
],
|
|
210
337
|
"description": "Token-cost class for this skill. `lean` (default): subject to telegraph-speak condensation and thin-projector trimming; optimize for <500 tokens. `standard`: balanced guidance, normal condensation. `rich`: exempt from telegraph-speak and thin-projector trimming; detailed examples preserved; eager-load eligible; ≤15% of skills in a suite may be `rich` (CI-enforced). A `rich` skill MUST carry a '## Why this skill is rich' section explaining the irreducible complexity that makes condensation harmful. Governed by `token-budget-discipline` rule + `tokens.rich_skills` consumer setting. See docs/guidelines/token-budget-governance.md."
|
|
211
338
|
},
|
|
212
339
|
"parallelizable": {
|
|
213
340
|
"type": "string",
|
|
214
|
-
"enum": [
|
|
341
|
+
"enum": [
|
|
342
|
+
"steps",
|
|
343
|
+
"files",
|
|
344
|
+
"independent"
|
|
345
|
+
],
|
|
215
346
|
"description": "Classifier signal for auto-dispatch. 'steps' = ordered multi-step plan → do-in-steps. 'files' = N independent files of the same shape → do-in-parallel. 'independent' = N independent same-shape targets (not files) → do-in-parallel. Omit for skills that are always single-pass. Read by auto-dispatch-classification.md classifier signal 1."
|
|
216
347
|
},
|
|
217
348
|
"gaps": {
|
|
@@ -220,7 +351,10 @@
|
|
|
220
351
|
"items": {
|
|
221
352
|
"type": "object",
|
|
222
353
|
"additionalProperties": false,
|
|
223
|
-
"required": [
|
|
354
|
+
"required": [
|
|
355
|
+
"description",
|
|
356
|
+
"witness"
|
|
357
|
+
],
|
|
224
358
|
"properties": {
|
|
225
359
|
"description": {
|
|
226
360
|
"type": "string",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
LARGE_DIFF_LINES,
|
|
4
5
|
classifyBlocking,
|
|
6
|
+
escalationReasons,
|
|
5
7
|
gateVerdict,
|
|
6
8
|
isReviewablePath,
|
|
7
9
|
renderReview,
|
|
@@ -74,3 +76,49 @@ describe('renderReview', () => {
|
|
|
74
76
|
expect((out.match(/\(Blocking\)/g) ?? []).length).toBe(1);
|
|
75
77
|
});
|
|
76
78
|
});
|
|
79
|
+
|
|
80
|
+
describe('escalationReasons', () => {
|
|
81
|
+
it('flags a large diff at or above the threshold', () => {
|
|
82
|
+
expect(escalationReasons(['src/a.ts'], LARGE_DIFF_LINES)).toEqual([
|
|
83
|
+
`large diff (${LARGE_DIFF_LINES} changed lines ≥ ${LARGE_DIFF_LINES})`,
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
it('does not flag a small non-claim diff', () => {
|
|
87
|
+
expect(escalationReasons(['src/a.ts'], LARGE_DIFF_LINES - 1)).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
it('flags a claim-ledger surface regardless of size', () => {
|
|
90
|
+
expect(escalationReasons(['docs/CLAIMS.md'], 1)).toEqual([
|
|
91
|
+
'claim-affecting surface touched (docs/CLAIMS.md)',
|
|
92
|
+
]);
|
|
93
|
+
expect(escalationReasons(['docs/proof.md'], 1)[0]).toContain('docs/proof.md');
|
|
94
|
+
expect(escalationReasons(['docs/comparison.yaml'], 1)[0]).toContain('docs/comparison.yaml');
|
|
95
|
+
expect(escalationReasons(['README.md'], 1)[0]).toContain('README.md');
|
|
96
|
+
});
|
|
97
|
+
it('reports both reasons when a large diff also touches a claim surface', () => {
|
|
98
|
+
const r = escalationReasons(['docs/CLAIMS.md', 'src/big.ts'], LARGE_DIFF_LINES + 50);
|
|
99
|
+
expect(r).toHaveLength(2);
|
|
100
|
+
expect(r[0]).toContain('large diff');
|
|
101
|
+
expect(r[1]).toContain('claim-affecting');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('renderReview — escalation banner', () => {
|
|
106
|
+
const f = (severity: Finding['severity'], kind: Finding['kind']): Finding => ({
|
|
107
|
+
severity, kind, title: 't', detail: 'd',
|
|
108
|
+
});
|
|
109
|
+
it('appends the escalation recommendation when reasons are present', () => {
|
|
110
|
+
const out = renderReview([], false, ['large diff (500 changed lines ≥ 400)']);
|
|
111
|
+
expect(out).toContain('Escalation warranted');
|
|
112
|
+
expect(out).toContain('/council:pr');
|
|
113
|
+
expect(out).toContain('never');
|
|
114
|
+
});
|
|
115
|
+
it('omits the escalation block when there are no reasons (byte-identical to no-arg)', () => {
|
|
116
|
+
const findings = [f('high', 'security')];
|
|
117
|
+
expect(renderReview(findings, false, [])).toBe(renderReview(findings, false));
|
|
118
|
+
});
|
|
119
|
+
it('recommends escalation even on a clean (no-finding) large diff', () => {
|
|
120
|
+
const out = renderReview([], false, ['large diff (500 changed lines ≥ 400)']);
|
|
121
|
+
expect(out).toContain('no findings');
|
|
122
|
+
expect(out).toContain('Escalation warranted');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
* advisory mode it NEVER blocks the merge (exit 0) — it records what WOULD
|
|
13
13
|
* block. If no key is available it is a logged no-op (exit 0), never a failure.
|
|
14
14
|
*
|
|
15
|
+
* On a LARGE or CLAIM-AFFECTING diff the two in-session lenses are not enough —
|
|
16
|
+
* such a diff warrants the full `ai-council` advisor panel. That is a
|
|
17
|
+
* spend-bearing multi-model run, so per blocker `self-review-gate-cost` the gate
|
|
18
|
+
* DETECTS the escalation deterministically (`escalationReasons`) and RECOMMENDS
|
|
19
|
+
* a maintainer `/council:pr` run; it never fires paid council calls itself. The
|
|
20
|
+
* paid council stays governed by the standing spend-authorization discipline at
|
|
21
|
+
* run time.
|
|
22
|
+
*
|
|
15
23
|
* Turning the gate REQUIRED (block-on-verdict) and wiring the API secret + the
|
|
16
24
|
* per-PR budget are the maintainer's acts (blocker `self-review-gate-cost`); this
|
|
17
25
|
* script exposes the `--enforce` path so that flip is a one-flag change, but the
|
|
@@ -100,6 +108,47 @@ function diffText(baseRef: string, files: string[]): string {
|
|
|
100
108
|
return (r.stdout ?? '').toString();
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
/** Sum of added+deleted lines across `files` (binary files count 0). */
|
|
112
|
+
function changedLineCount(baseRef: string, files: string[]): number {
|
|
113
|
+
if (files.length === 0) return 0;
|
|
114
|
+
const r = spawnSync('git', ['diff', '--numstat', `${baseRef}...HEAD`, '--', ...files], {
|
|
115
|
+
cwd: REPO_ROOT,
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
118
|
+
});
|
|
119
|
+
let total = 0;
|
|
120
|
+
for (const line of (r.stdout ?? '').toString().split('\n')) {
|
|
121
|
+
const m = /^(\d+)\t(\d+)\t/.exec(line); // added \t deleted \t path ("-" for binary)
|
|
122
|
+
if (m) total += Number(m[1]) + Number(m[2]);
|
|
123
|
+
}
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Escalation classifier (large / claim-affecting → full ai-council) ──
|
|
128
|
+
/**
|
|
129
|
+
* A large or claim-affecting diff warrants the FULL `ai-council` (advisor
|
|
130
|
+
* panel), not just the two in-session lenses — but that is a spend-bearing,
|
|
131
|
+
* multi-model run. Per blocker `self-review-gate-cost` the paid council stays
|
|
132
|
+
* governed by the standing spend-authorization discipline AT RUN TIME: the gate
|
|
133
|
+
* DETECTS the escalation deterministically here and RECOMMENDS a maintainer
|
|
134
|
+
* `/council:pr` run; it never fires paid council calls by surprise in CI.
|
|
135
|
+
*/
|
|
136
|
+
export const LARGE_DIFF_LINES = 400;
|
|
137
|
+
/** The claim ledger + proof surfaces `check_claims` binds to. */
|
|
138
|
+
const CLAIM_SURFACES = new Set(['docs/CLAIMS.md', 'docs/proof.md', 'docs/comparison.yaml']);
|
|
139
|
+
|
|
140
|
+
export function escalationReasons(files: readonly string[], changedLines: number): string[] {
|
|
141
|
+
const reasons: string[] = [];
|
|
142
|
+
if (changedLines >= LARGE_DIFF_LINES) {
|
|
143
|
+
reasons.push(`large diff (${changedLines} changed lines ≥ ${LARGE_DIFF_LINES})`);
|
|
144
|
+
}
|
|
145
|
+
const claimHits = files.filter((f) => CLAIM_SURFACES.has(f) || f === 'README.md');
|
|
146
|
+
if (claimHits.length > 0) {
|
|
147
|
+
reasons.push(`claim-affecting surface touched (${claimHits.join(', ')})`);
|
|
148
|
+
}
|
|
149
|
+
return reasons;
|
|
150
|
+
}
|
|
151
|
+
|
|
103
152
|
// ── Review skill bodies (the system prompt) ───────────────────────────
|
|
104
153
|
const REVIEW_SKILLS = ['adversarial-review', 'agent-security-review'] as const;
|
|
105
154
|
|
|
@@ -129,15 +178,18 @@ export interface ReviewPlan {
|
|
|
129
178
|
files: string[];
|
|
130
179
|
promptChars: number;
|
|
131
180
|
note: string;
|
|
181
|
+
escalation: string[];
|
|
132
182
|
}
|
|
133
183
|
|
|
134
184
|
export function buildPlan(baseRef: string): ReviewPlan {
|
|
135
185
|
const files = changedFiles(baseRef);
|
|
136
186
|
const diff = diffText(baseRef, files);
|
|
187
|
+
const escalation = escalationReasons(files, changedLineCount(baseRef, files));
|
|
137
188
|
return {
|
|
138
189
|
skills: [...REVIEW_SKILLS],
|
|
139
190
|
files,
|
|
140
191
|
promptChars: buildSystemPrompt().length + diff.length,
|
|
192
|
+
escalation,
|
|
141
193
|
note:
|
|
142
194
|
files.length === 0
|
|
143
195
|
? 'No reviewable (non-generated) files changed — the live review would no-op.'
|
|
@@ -174,13 +226,26 @@ function parseFindings(text: string): Finding[] {
|
|
|
174
226
|
}));
|
|
175
227
|
}
|
|
176
228
|
|
|
177
|
-
|
|
229
|
+
/** Advisory escalation banner, appended when a diff warrants full ai-council. */
|
|
230
|
+
function escalationBlock(reasons: readonly string[]): string {
|
|
231
|
+
if (reasons.length === 0) return '';
|
|
232
|
+
return (
|
|
233
|
+
`\n\n🔺 **Escalation warranted** — ${reasons.join('; ')}. The two in-session ` +
|
|
234
|
+
'lenses above are a floor; a maintainer should run the full `ai-council` ' +
|
|
235
|
+
'(`/council:pr`) on this diff. That is a spend-bearing, run-time-authorized ' +
|
|
236
|
+
'act (blocker `self-review-gate-cost`) — this gate recommends it, never ' +
|
|
237
|
+
'fires paid council calls itself.'
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function renderReview(findings: Finding[], enforce: boolean, escalation: readonly string[] = []): string {
|
|
178
242
|
const banner =
|
|
179
243
|
'> HUMAN REVIEW REQUIRED — dogfooded AI adversarial-review + security gate. ' +
|
|
180
244
|
'Findings are decision support, not a guarantee; detection is probabilistic. ' +
|
|
181
245
|
'This is a floor, **not** independent human review.';
|
|
246
|
+
const esc = escalationBlock(escalation);
|
|
182
247
|
if (findings.length === 0) {
|
|
183
|
-
return `${banner}\n\n✅ Self-review gate: no findings
|
|
248
|
+
return `${banner}\n\n✅ Self-review gate: no findings.${esc}`;
|
|
184
249
|
}
|
|
185
250
|
const blocking = findings.filter(classifyBlocking);
|
|
186
251
|
// Each row carries an explicit (Blocking)/(Advisory) marker so a
|
|
@@ -197,7 +262,7 @@ export function renderReview(findings: Finding[], enforce: boolean): string {
|
|
|
197
262
|
? `❌ ${blocking.length} merge-blocking finding(s) (security/claim × high+).`
|
|
198
263
|
: `⚠️ ${blocking.length} finding(s) WOULD block merge under an enforced gate (advisory now).`
|
|
199
264
|
: '✅ No merge-blocking findings (style/correctness advise only).';
|
|
200
|
-
return `${banner}\n\n${verdictLine}\n\n| severity | kind | file | finding |\n|---|---|---|---|\n${rows}`;
|
|
265
|
+
return `${banner}\n\n${verdictLine}\n\n| severity | kind | file | finding |\n|---|---|---|---|\n${rows}${esc}`;
|
|
201
266
|
}
|
|
202
267
|
|
|
203
268
|
function postReview(body: string): void {
|
|
@@ -238,7 +303,10 @@ export function main(argv: string[]): 0 | 2 {
|
|
|
238
303
|
`self-review-gate (dry-run — no spend, advisory):\n` +
|
|
239
304
|
` skills: ${plan.skills.join(', ')}\n` +
|
|
240
305
|
` files: ${plan.files.length}\n` +
|
|
241
|
-
` ${plan.note}\n
|
|
306
|
+
` ${plan.note}\n` +
|
|
307
|
+
(plan.escalation.length
|
|
308
|
+
? ` escalation: ${plan.escalation.join('; ')} → recommend /council:pr (run-time authorized)\n`
|
|
309
|
+
: ` escalation: none (diff is small and not claim-affecting)\n`),
|
|
242
310
|
);
|
|
243
311
|
return 0;
|
|
244
312
|
}
|
|
@@ -282,7 +350,7 @@ export function main(argv: string[]): 0 | 2 {
|
|
|
282
350
|
return 0; // no credit / transport / API error → never blocks the merge
|
|
283
351
|
}
|
|
284
352
|
const findings = parseFindings(resp.text);
|
|
285
|
-
const body = renderReview(findings, enforce);
|
|
353
|
+
const body = renderReview(findings, enforce, plan.escalation);
|
|
286
354
|
postReview(body);
|
|
287
355
|
|
|
288
356
|
const code = gateVerdict(findings, { enforce });
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Validate the optional `team:` annotation on flows (`src/flows/*.yaml`).
|
|
4
|
+
*
|
|
5
|
+
* A demand-probe primitive (road-to-persona-library-harvest.md Phase 3): a flow
|
|
6
|
+
* MAY declare `team: { personas: [...], skills: [...] }` naming the persona/skill
|
|
7
|
+
* team its work typically pulls in. This validator checks referential
|
|
8
|
+
* integrity — every persona id resolves to `src/agent-src/personas/<id>.md`,
|
|
9
|
+
* every skill slug to a real `skills/<slug>/SKILL.md` — so a typo is caught, but
|
|
10
|
+
* it adds NO execution semantics (subagent-orchestration stays the executor) and
|
|
11
|
+
* no new artifact class. If organic use grows (≥ 5 flows carrying `team:` with a
|
|
12
|
+
* repeated conditional shape), THAT is the signal to design a roster schema —
|
|
13
|
+
* not before.
|
|
14
|
+
*
|
|
15
|
+
* Exit 0 clean (incl. no flow carrying `team:`) · 1 on an unresolved reference
|
|
16
|
+
* · 2 usage.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
21
|
+
|
|
22
|
+
import YAML from 'yaml';
|
|
23
|
+
|
|
24
|
+
import { resolve_logical } from './_lib/agent_src.js';
|
|
25
|
+
|
|
26
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
27
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
28
|
+
const FLOWS_DIR = path.join(ROOT, 'src', 'flows');
|
|
29
|
+
const PERSONAS_DIR = path.join(ROOT, 'src', 'agent-src', 'personas');
|
|
30
|
+
const NON_FLOW = new Set(['surface-map.yaml', 'cookbook.yaml']);
|
|
31
|
+
|
|
32
|
+
function _isFile(p: string): boolean {
|
|
33
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
function _rel(p: string): string {
|
|
36
|
+
return path.relative(ROOT, p).split(path.sep).join('/');
|
|
37
|
+
}
|
|
38
|
+
function _personaExists(id: string): boolean {
|
|
39
|
+
return _isFile(path.join(PERSONAS_DIR, `${id}.md`));
|
|
40
|
+
}
|
|
41
|
+
function _skillExists(slug: string): boolean {
|
|
42
|
+
return resolve_logical(`skills/${slug}/SKILL.md`) !== null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Violation { file: string; reason: string; }
|
|
46
|
+
|
|
47
|
+
export function checkFile(abs: string): Violation[] {
|
|
48
|
+
const rel = _rel(abs);
|
|
49
|
+
let data: unknown;
|
|
50
|
+
try {
|
|
51
|
+
data = YAML.parse(fs.readFileSync(abs, 'utf-8'), { version: '1.1' });
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return [{ file: rel, reason: `not valid YAML: ${e instanceof Error ? e.message : String(e)}` }];
|
|
54
|
+
}
|
|
55
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) return [];
|
|
56
|
+
const team = (data as Record<string, unknown>)['team'];
|
|
57
|
+
if (team === undefined || team === null) return [];
|
|
58
|
+
if (typeof team !== 'object' || Array.isArray(team)) {
|
|
59
|
+
return [{ file: rel, reason: '`team` must be a mapping with optional `personas` / `skills` arrays' }];
|
|
60
|
+
}
|
|
61
|
+
const vios: Violation[] = [];
|
|
62
|
+
const personas = (team as Record<string, unknown>)['personas'];
|
|
63
|
+
for (const id of Array.isArray(personas) ? personas : []) {
|
|
64
|
+
if (typeof id === 'string' && !_personaExists(id)) {
|
|
65
|
+
vios.push({ file: rel, reason: `team.personas: persona '${id}' does not resolve (no src/agent-src/personas/${id}.md)` });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const skills = (team as Record<string, unknown>)['skills'];
|
|
69
|
+
for (const slug of Array.isArray(skills) ? skills : []) {
|
|
70
|
+
if (typeof slug === 'string' && !_skillExists(slug)) {
|
|
71
|
+
vios.push({ file: rel, reason: `team.skills: skill '${slug}' does not resolve (no skills/${slug}/SKILL.md)` });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return vios;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function main(argv?: string[]): number {
|
|
78
|
+
const args = argv ?? process.argv.slice(2);
|
|
79
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
80
|
+
process.stdout.write('usage: validate_flow_teams [--quiet]\n');
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const unknown = args.filter((a) => a !== '--quiet');
|
|
84
|
+
if (unknown.length > 0) {
|
|
85
|
+
process.stderr.write(`unrecognized argument: ${unknown[0]}\nusage: validate_flow_teams [--quiet]\n`);
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
const quiet = args.includes('--quiet');
|
|
89
|
+
|
|
90
|
+
let files: string[];
|
|
91
|
+
try {
|
|
92
|
+
files = fs.readdirSync(FLOWS_DIR).filter((n) => n.endsWith('.yaml') && !NON_FLOW.has(n)).sort();
|
|
93
|
+
} catch {
|
|
94
|
+
process.stderr.write(`flows dir not found: ${FLOWS_DIR}\n`);
|
|
95
|
+
return 0; // nothing to validate
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const vios: Violation[] = [];
|
|
99
|
+
let annotated = 0;
|
|
100
|
+
for (const name of files) {
|
|
101
|
+
const abs = path.join(FLOWS_DIR, name);
|
|
102
|
+
vios.push(...checkFile(abs));
|
|
103
|
+
if (/^team:/m.test(fs.readFileSync(abs, 'utf-8'))) annotated++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (vios.length > 0) {
|
|
107
|
+
process.stderr.write(`validate_flow_teams: ${vios.length} unresolved reference(s):\n`);
|
|
108
|
+
for (const v of vios) process.stderr.write(` ${v.file}: ${v.reason}\n`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
if (!quiet) {
|
|
112
|
+
process.stdout.write(`validate_flow_teams: OK — ${annotated} flow(s) carry a \`team:\` annotation, all references resolve.\n`);
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _isCliEntry(): boolean {
|
|
118
|
+
if (process.argv[1] === undefined) return false;
|
|
119
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
120
|
+
try {
|
|
121
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
122
|
+
} catch { return false; }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (_isCliEntry()) {
|
|
126
|
+
process.exit(main());
|
|
127
|
+
}
|