@event4u/agent-config 2.7.0 → 2.9.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 (76) hide show
  1. package/.agent-src/personas/cmo.md +122 -0
  2. package/.agent-src/personas/customer-success-lead.md +126 -0
  3. package/.agent-src/personas/engineering-manager.md +133 -0
  4. package/.agent-src/personas/finance-partner.md +129 -0
  5. package/.agent-src/personas/growth-pm.md +134 -0
  6. package/.agent-src/personas/people-strategist.md +126 -0
  7. package/.agent-src/personas/revops.md +125 -0
  8. package/.agent-src/personas/strategist.md +129 -0
  9. package/.agent-src/skills/activation-design/SKILL.md +160 -0
  10. package/.agent-src/skills/build-buy-partner/SKILL.md +145 -0
  11. package/.agent-src/skills/churn-prevention/SKILL.md +156 -0
  12. package/.agent-src/skills/comp-banding/SKILL.md +160 -0
  13. package/.agent-src/skills/competitive-moat-analysis/SKILL.md +152 -0
  14. package/.agent-src/skills/content-funnel-design/SKILL.md +170 -0
  15. package/.agent-src/skills/contracts-cognition/SKILL.md +147 -0
  16. package/.agent-src/skills/data-handling-judgment/SKILL.md +155 -0
  17. package/.agent-src/skills/deal-qualification-meddic/SKILL.md +165 -0
  18. package/.agent-src/skills/editorial-calendar/SKILL.md +161 -0
  19. package/.agent-src/skills/expansion-playbook/SKILL.md +171 -0
  20. package/.agent-src/skills/forecast-accuracy/SKILL.md +157 -0
  21. package/.agent-src/skills/forecasting/SKILL.md +164 -0
  22. package/.agent-src/skills/fundraising-narrative/SKILL.md +189 -0
  23. package/.agent-src/skills/funnel-analysis/SKILL.md +26 -2
  24. package/.agent-src/skills/gtm-launch/SKILL.md +165 -0
  25. package/.agent-src/skills/hiring-loop-design/SKILL.md +167 -0
  26. package/.agent-src/skills/market-entry-analysis/SKILL.md +144 -0
  27. package/.agent-src/skills/messaging-architecture/SKILL.md +184 -0
  28. package/.agent-src/skills/onboarding-design/SKILL.md +158 -0
  29. package/.agent-src/skills/onboarding-program/SKILL.md +157 -0
  30. package/.agent-src/skills/one-on-one-cadence/SKILL.md +161 -0
  31. package/.agent-src/skills/org-design/SKILL.md +158 -0
  32. package/.agent-src/skills/perf-feedback-craft/SKILL.md +157 -0
  33. package/.agent-src/skills/pipeline-strategy/SKILL.md +159 -0
  34. package/.agent-src/skills/positioning-strategy/SKILL.md +177 -0
  35. package/.agent-src/skills/privacy-review/SKILL.md +160 -0
  36. package/.agent-src/skills/retention-loops/SKILL.md +161 -0
  37. package/.agent-src/skills/runway-cognition/SKILL.md +136 -0
  38. package/.agent-src/skills/scenario-modeling/SKILL.md +139 -0
  39. package/.agent-src/skills/subagent-orchestration/SKILL.md +1 -1
  40. package/.agent-src/skills/throughput-vs-morale-tradeoff/SKILL.md +165 -0
  41. package/.agent-src/skills/unit-economics-modeling/SKILL.md +54 -7
  42. package/.agent-src/skills/vision-articulation/SKILL.md +146 -0
  43. package/.agent-src/skills/voice-and-tone-design/SKILL.md +163 -0
  44. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  45. package/.agent-src/templates/scripts/telemetry/settings.py +65 -0
  46. package/.agent-src/templates/scripts/tier_usage_report.py +183 -0
  47. package/.claude-plugin/marketplace.json +34 -2
  48. package/AGENTS.md +1 -1
  49. package/CHANGELOG.md +135 -153
  50. package/README.md +3 -3
  51. package/docs/architecture.md +37 -11
  52. package/docs/archive/CHANGELOG-pre-2.7.0.md +185 -0
  53. package/docs/catalog.md +38 -4
  54. package/docs/contracts/adr-forecast-construction-shape.md +89 -0
  55. package/docs/contracts/adr-gtm-context-spine.md +115 -0
  56. package/docs/contracts/adr-wing4-context-spine.md +125 -0
  57. package/docs/contracts/command-clusters.md +41 -0
  58. package/docs/contracts/command-surface-tiers.md +30 -9
  59. package/docs/contracts/context-spine.md +58 -12
  60. package/docs/contracts/cross-wing-handoff.md +3 -3
  61. package/docs/contracts/mcp-beta-criteria.md +129 -0
  62. package/docs/contracts/persona-schema.md +20 -3
  63. package/docs/guidelines/gtm-handoff.md +114 -0
  64. package/docs/guidelines/wing4-handoff.md +127 -0
  65. package/docs/mcp-server.md +1 -1
  66. package/package.json +1 -1
  67. package/scripts/_cli/cmd_doctor.py +527 -14
  68. package/scripts/_cli/cmd_validate.py +10 -0
  69. package/scripts/agent-config +19 -18
  70. package/scripts/install.py +5 -0
  71. package/scripts/lint_context_spine_usage.py +5 -1
  72. package/scripts/mcp_server/__init__.py +1 -0
  73. package/scripts/mcp_server/server.py +4 -3
  74. package/scripts/schemas/persona.schema.json +5 -0
  75. package/scripts/schemas/skill.schema.json +2 -2
  76. package/scripts/skill_linter.py +284 -6
@@ -62,17 +62,6 @@ Tier 0 — daily-driver (init → sync → validate → work):
62
62
  (Option-A loop; called by the /work command)
63
63
  implement-ticket Drive the work_engine Python engine on a ticket envelope
64
64
  (Option-A loop; called by the /implement-ticket command)
65
- first-run Guided first-run setup — cost profile, settings, tooling
66
- keys:install-anthropic Install the Anthropic API key for the AI Council
67
- (interactive, /dev/tty only, writes ~/.config/agent-config/anthropic.key 0600)
68
- keys:install-openai Install the OpenAI API key for the AI Council
69
- (interactive, /dev/tty only, writes ~/.config/agent-config/openai.key 0600)
70
- council:estimate Pre-call council cost preview (no API call, no spend)
71
- Usage: council:estimate <question> [--input-mode prompt|roadmap]
72
- council:run Run the council. Requires --confirm to spend.
73
- Usage: council:run <question> --output <path> --confirm
74
- council:render Re-render a saved council responses JSON to markdown
75
- Usage: council:render <responses.json>
76
65
  help Show this help (default Tier-0; --tier=1|all expands)
77
66
  --version, -V Print package version
78
67
  EOF
@@ -110,6 +99,17 @@ Tier 1 — power-user (release shape, audit, migration):
110
99
  Flags: --json | --project=<path>
111
100
  migrate One-shot migration off legacy composer / npm install paths
112
101
  Flags: --dry-run (detect only)
102
+ first-run Guided first-run setup — cost profile, settings, tooling
103
+ keys:install-anthropic Install the Anthropic API key for the AI Council
104
+ (interactive, /dev/tty only, writes ~/.config/agent-config/anthropic.key 0600)
105
+ keys:install-openai Install the OpenAI API key for the AI Council
106
+ (interactive, /dev/tty only, writes ~/.config/agent-config/openai.key 0600)
107
+ council:estimate Pre-call council cost preview (no API call, no spend)
108
+ Usage: council:estimate <question> [--input-mode prompt|roadmap]
109
+ council:run Run the council. Requires --confirm to spend.
110
+ Usage: council:run <question> --output <path> --confirm
111
+ council:render Re-render a saved council responses JSON to markdown
112
+ Usage: council:render <responses.json>
113
113
  EOF
114
114
  fi
115
115
 
@@ -124,6 +124,7 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
124
124
  (one-line MCP server onboarding; idempotent)
125
125
  mcp:run Run the built-in MCP server over stdio
126
126
  (requires `mcp:setup` first; see docs/mcp-server.md)
127
+ (experimental — beta gates: docs/contracts/mcp-beta-criteria.md)
127
128
  roadmap:progress Regenerate agents/roadmaps-progress.md from open roadmaps
128
129
  roadmap:progress-check Fail if agents/roadmaps-progress.md is stale (for CI)
129
130
  hooks:install Install the pre-commit roadmap-progress hook
@@ -161,7 +162,7 @@ EOF
161
162
  if [[ "$tier" == "0" ]]; then
162
163
  cat <<'EOF'
163
164
 
164
- (Hidden: 9 Tier-1 + 26 Tier-2 commands. Run `./agent-config --help --tier=1`
165
+ (Hidden: 15 Tier-1 + 26 Tier-2 commands. Run `./agent-config --help --tier=1`
165
166
  or `--tier=all` to see them. Tier criteria: docs/contracts/command-surface-tiers.md.)
166
167
  EOF
167
168
  fi
@@ -173,14 +174,8 @@ Examples (Tier 0):
173
174
  ./agent-config sync --dry-run
174
175
  ./agent-config sync
175
176
  ./agent-config validate
176
- ./agent-config first-run
177
177
  ./agent-config work --state-file .work-state.json --prompt-file prompt.txt
178
178
  ./agent-config implement-ticket --state-file .work-state.json
179
- ./agent-config keys:install-anthropic
180
- ./agent-config keys:install-openai
181
- ./agent-config council:estimate prompt.txt
182
- ./agent-config council:run prompt.txt --output agents/council-sessions/out.json --confirm
183
- ./agent-config council:render agents/council-sessions/out.json
184
179
  EOF
185
180
 
186
181
  if [[ "$tier" == "1" || "$tier" == "all" ]]; then
@@ -203,6 +198,12 @@ Examples (Tier 1):
203
198
  ./agent-config versions --json
204
199
  ./agent-config init --offline --tools=claude-code,cursor --yes
205
200
  ./agent-config update --offline --to=2.2.0
201
+ ./agent-config first-run
202
+ ./agent-config keys:install-anthropic
203
+ ./agent-config keys:install-openai
204
+ ./agent-config council:estimate prompt.txt
205
+ ./agent-config council:run prompt.txt --output agents/council-sessions/out.json --confirm
206
+ ./agent-config council:render agents/council-sessions/out.json
206
207
  EOF
207
208
  fi
208
209
 
@@ -105,6 +105,11 @@ def warn(msg: str) -> None:
105
105
 
106
106
  def fail(msg: str) -> "None":
107
107
  print(f" ❌ {msg}", file=sys.stderr)
108
+ print(
109
+ " Diagnose: `./agent-config doctor` "
110
+ "(or `--check <id>` for a single category)",
111
+ file=sys.stderr,
112
+ )
108
113
  sys.exit(1)
109
114
 
110
115
 
@@ -31,7 +31,11 @@ SKILL_GLOBS = (
31
31
  ".agent-src.uncompressed/skills/**/SKILL.md",
32
32
  ".agent-src/skills/**/SKILL.md",
33
33
  )
34
- VALID_SLOTS = ("product", "team", "repo")
34
+ VALID_SLOTS = (
35
+ "product", "team", "repo",
36
+ "channel-stage", "funnel-stage", "customer-segment",
37
+ "fiscal-period", "org-stage", "regulatory-regime",
38
+ )
35
39
 
36
40
  CONTEXT_SPINE_PAT = re.compile(
37
41
  r"^context_spine:\s*\[([^\]]*)\]\s*$", re.MULTILINE
@@ -11,6 +11,7 @@ boundary in `agents/roadmaps/road-to-mcp-server.md`. No `tools`
11
11
  primitive, no engine spawn, no shell execution.
12
12
 
13
13
  Stability: experimental. Contract: `docs/contracts/mcp-phase-1-scope.md`.
14
+ Promotion to beta gated on `docs/contracts/mcp-beta-criteria.md`.
14
15
  """
15
16
  from __future__ import annotations
16
17
 
@@ -125,9 +125,10 @@ def build_server(
125
125
  name=SERVER_NAME,
126
126
  version=__version__,
127
127
  instructions=(
128
- "agent-config MCP server (Phase 3, experimental). Exposes "
129
- "all skills + commands as instructional prompts, plus "
130
- "rules + guidelines + contexts as read-only resources."
128
+ "agent-config MCP server (Phase 3, experimental; beta gates "
129
+ "in docs/contracts/mcp-beta-criteria.md). Exposes all skills "
130
+ "+ commands as instructional prompts, plus rules + guidelines "
131
+ "+ contexts as read-only resources."
131
132
  ),
132
133
  )
133
134
 
@@ -26,6 +26,11 @@
26
26
  "type": "string",
27
27
  "enum": ["core", "specialist"]
28
28
  },
29
+ "wing": {
30
+ "type": "integer",
31
+ "enum": [1, 2, 3, 4],
32
+ "description": "Cognition wing per docs/contracts/package-self-orientation.md § The four wings. Optional. Raises the specialist size cap (persona-schema § 4)."
33
+ },
29
34
  "mode": {
30
35
  "type": "string",
31
36
  "enum": ["developer", "reviewer", "tester", "product-owner", "incident", "planner"]
@@ -72,9 +72,9 @@
72
72
  "uniqueItems": true,
73
73
  "items": {
74
74
  "type": "string",
75
- "enum": ["product", "team", "repo"]
75
+ "enum": ["product", "team", "repo", "channel-stage", "funnel-stage", "customer-segment", "fiscal-period", "org-stage", "regulatory-regime"]
76
76
  },
77
- "description": "Senior-skill opt-in for the tri-slot context spine. Declares which slots under agents/context-spine/ the skill expects to read (product, team, repo). Council Q1 (KEEP-3) locks the slot count at 3; additions require 2 citing skills + ADR per docs/contracts/context-spine.md § 5."
77
+ "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."
78
78
  },
79
79
  "execution": {
80
80
  "type": "object",
@@ -57,6 +57,15 @@ REQUIRED_PERSONA_SECTIONS = REQUIRED_PERSONA_SECTIONS_CORE
57
57
  VALID_PERSONA_TIERS = {"core", "specialist"}
58
58
  # Locked in docs/contracts/persona-schema.md § 4: core ≤ 120, specialist ≤ 100.
59
59
  PERSONA_LINE_BUDGETS = {"core": 120, "specialist": 100}
60
+ # Wing-scoped overrides — Wing-3 (GTM) and Wing-4 (Money/Strategy/Ops) carry
61
+ # denser cognition (funnel × channel × lifecycle, or finance × org × strategy)
62
+ # than Wing-1/2 specialists, so the line cap rises to keep the seven-section
63
+ # spine intact without amputating workflows. Persona-schema.md § 4 wing matrix.
64
+ VALID_PERSONA_WINGS = {1, 2, 3, 4}
65
+ PERSONA_LINE_BUDGETS_BY_WING = {
66
+ ("specialist", 3): 140,
67
+ ("specialist", 4): 140,
68
+ }
60
69
 
61
70
 
62
71
  REQUIRED_SKILL_SECTIONS = [
@@ -161,6 +170,89 @@ VALID_EXECUTION_HANDLERS = {"none", "shell", "php", "node", "internal"}
161
170
  VALID_EXECUTION_SAFETY_MODES = {"strict"}
162
171
  VALID_EXECUTION_FIELDS = {"type", "handler", "timeout_seconds", "safety_mode", "allowed_tools", "command"}
163
172
 
173
+ # --- Wing-3 GTM cognition-boundary patterns (council Q7 / iter-2 OQ3) ---
174
+ # Triggered only when a skill's context_spine declares a Wing-3 slot.
175
+ # See docs/contracts/adr-gtm-context-spine.md and
176
+ # agents/roadmaps/road-to-gtm-and-growth.md § G2.
177
+ WING3_SPINE_SLOTS = {"channel-stage", "funnel-stage", "customer-segment"}
178
+
179
+ CONTEXT_SPINE_INLINE_PATTERN = re.compile(
180
+ r'^context_spine:\s*\[(.*?)\]\s*$', re.MULTILINE
181
+ )
182
+
183
+ # agent-operability: external SaaS URLs the agent would have to auth against
184
+ WING3_SAAS_URL_PATTERN = re.compile(
185
+ r"https?://[\w.-]*\.(salesforce|hubspot|marketo|pardot|mailchimp|"
186
+ r"intercom|amplitude|mixpanel|segment|klaviyo|sendgrid|mailgun|"
187
+ r"pendo|gong|outreach|salesloft|apollo)\.(com|io)\b",
188
+ re.IGNORECASE,
189
+ )
190
+
191
+ # vendor-independence: brand / SDK / platform slugs that lock cognition
192
+ WING3_VENDOR_BLACKLIST = re.compile(
193
+ r"\b(salesforce|hubspot|marketo|pardot|mailchimp|intercom|drift|"
194
+ r"klaviyo|sendgrid|mailgun|amplitude|mixpanel|pendo|gong|"
195
+ r"outreach\.io|salesloft|apollo\.io|zendesk|freshworks)\b",
196
+ re.IGNORECASE,
197
+ )
198
+
199
+ # transferability: stack-locked tooling instructions
200
+ WING3_STACK_LOCKED_PATTERN = re.compile(
201
+ r"\b(npm install|pip install|composer require|gem install|"
202
+ r"cargo add|yarn add|pnpm add|bundle add)\s+[\w@/.-]+",
203
+ re.IGNORECASE,
204
+ )
205
+
206
+ # channel-agnosticism: channel-specific tactical prescriptions
207
+ WING3_CHANNEL_TACTIC_PATTERN = re.compile(
208
+ r"\b(email subject line|tweet length|linkedin (post|ad)|"
209
+ r"facebook ad|google ads?|tiktok (post|video)|instagram (post|reel)|"
210
+ r"sms character limit|cold email template)\b",
211
+ re.IGNORECASE,
212
+ )
213
+
214
+ # --- Wing-4 Money/Strategy/Ops cognition-boundary patterns (council Q7 / J2) ---
215
+ # Triggered only when a skill's context_spine declares a Wing-4 slot.
216
+ # See docs/contracts/adr-wing4-context-spine.md and
217
+ # agents/roadmaps/road-to-money-strategy-ops.md § J2.
218
+ WING4_SPINE_SLOTS = {"fiscal-period", "org-stage", "regulatory-regime"}
219
+
220
+ # agent-operability: external finance / HR / legal SaaS URLs
221
+ WING4_SAAS_URL_PATTERN = re.compile(
222
+ r"https?://[\w.-]*\.(quickbooks|intuit|netsuite|xero|sage|"
223
+ r"carta|pulley|gusto|bamboohr|lattice|15five|justworks|"
224
+ r"docusign|ironclad|onetrust|rippling|workday|deel|"
225
+ r"namely|adp|paychex|trinet|hibob|cultureamp)\.(com|io|co)\b",
226
+ re.IGNORECASE,
227
+ )
228
+
229
+ # vendor-independence: finance / HR / legal brand / SDK slugs
230
+ WING4_VENDOR_BLACKLIST = re.compile(
231
+ r"\b(quickbooks|netsuite|xero|sage intacct|"
232
+ r"carta|pulley|gusto|bamboohr|lattice|15five|justworks|"
233
+ r"docusign|ironclad|onetrust|rippling|workday|deel|"
234
+ r"namely|adp|paychex|trinet|hibob|culture amp)\b",
235
+ re.IGNORECASE,
236
+ )
237
+
238
+ # stage-agnosticism: prescriptive stage-specific thresholds that lock cognition
239
+ # Catches hardcoded runway / ARR / burn / team-size prescriptions tied to a
240
+ # specific funding stage. Framework-style framing ("read the org-stage slot",
241
+ # "applies across seed and public") passes; hard prescriptions ("18 months of
242
+ # runway", "Series A teams must hire") fire.
243
+ WING4_STAGE_AGNOSTIC_PATTERN = re.compile(
244
+ r"(?:"
245
+ r"\b\d+\s+months?\s+of\s+runway\b"
246
+ r"|\brunway\s+of\s+at\s+least\s+\d+\s+months?\b"
247
+ r"|\bminimum\s+runway\s+of\s+\d+\b"
248
+ r"|\b(?:seed|series\s+[a-d]|growth|pre-?ipo|post-?ipo)[-\s]stage\s+"
249
+ r"(?:companies|startups|teams|founders|orgs)\s+(?:must|should|always|never)\b"
250
+ r"|\bteam\s+of\s+\d+\s+(?:or\s+more|or\s+fewer)\b"
251
+ r"|\b(?:arr|mrr|burn\s+rate)\s+(?:of|over|under|above|below)\s+\$\d+"
252
+ r")",
253
+ re.IGNORECASE,
254
+ )
255
+
164
256
 
165
257
  @dataclass
166
258
  class Issue:
@@ -569,9 +661,9 @@ def lint_skill(path: Path, text: str) -> LintResult:
569
661
  skill_name = path.parent.name if path.name == "SKILL.md" else path.stem
570
662
  if skill_name and "-" not in skill_name and len(skill_name) >= 3:
571
663
  # Single word without qualifier — likely too generic
572
- ALLOWED_BARE_NOUNS = {"database", "devcontainer", "docker", "eloquent", "flux", "grafana",
573
- "laravel", "livewire", "mcp", "openapi", "performance", "security",
574
- "terraform", "terragrunt", "traefik", "websocket"}
664
+ ALLOWED_BARE_NOUNS = {"database", "devcontainer", "docker", "eloquent", "flux", "forecasting",
665
+ "grafana", "laravel", "livewire", "mcp", "openapi", "performance",
666
+ "security", "terraform", "terragrunt", "traefik", "websocket"}
575
667
  if skill_name.lower() not in ALLOWED_BARE_NOUNS:
576
668
  issues.append(Issue("warning", "bare_noun_name",
577
669
  f"Bare-noun skill name `{skill_name}` — consider adding a qualifier (e.g., `{skill_name}-management`)"))
@@ -605,6 +697,15 @@ def lint_skill(path: Path, text: str) -> LintResult:
605
697
  if tier_match and tier_match.group(1) == "senior":
606
698
  issues.extend(lint_senior_tier_blocks(text))
607
699
 
700
+ # --- Wing-3 GTM cognition-boundary check (council Q7 / adr-gtm-context-spine.md) ---
701
+ spine_slots = parse_context_spine(frontmatter)
702
+ if spine_slots and any(s in WING3_SPINE_SLOTS for s in spine_slots):
703
+ issues.extend(lint_wing3_boundaries(text))
704
+
705
+ # --- Wing-4 Money/Strategy/Ops cognition-boundary check (council Q7 / J2) ---
706
+ if spine_slots and any(s in WING4_SPINE_SLOTS for s in spine_slots):
707
+ issues.extend(lint_wing4_boundaries(text))
708
+
608
709
  procedure_block = find_procedure_block(text)
609
710
  if procedure_block is not None:
610
711
  if not procedure_block:
@@ -993,6 +1094,156 @@ def lint_senior_tier_blocks(text: str) -> List[Issue]:
993
1094
  return issues
994
1095
 
995
1096
 
1097
+ def parse_context_spine(frontmatter: str) -> Optional[List[str]]:
1098
+ """Parse `context_spine:` from frontmatter.
1099
+
1100
+ Supports the inline form `context_spine: [a, b, c]` (most skills) and
1101
+ the block form via `_parse_yaml_list`. Returns the slot list, ``[]``
1102
+ for an explicitly empty array, or ``None`` if the key is absent.
1103
+ """
1104
+ match = CONTEXT_SPINE_INLINE_PATTERN.search(frontmatter)
1105
+ if match is not None:
1106
+ inner = match.group(1).strip()
1107
+ if not inner:
1108
+ return []
1109
+ return [s.strip().strip('"').strip("'") for s in inner.split(",") if s.strip()]
1110
+ block = _parse_yaml_list(frontmatter, "context_spine")
1111
+ return block
1112
+
1113
+
1114
+ def _strip_wing3_carve_outs(text: str) -> str:
1115
+ """Remove fenced code, inline backticks, the ``## Do NOT`` block, and
1116
+ ``**WHEN NOT to use this**`` bullets so legitimate citations of vendor
1117
+ names (as off-scope examples) do not trip Wing-3 boundary checks.
1118
+ """
1119
+ text = re.sub(r"```[^\n]*\n.*?```", "", text, flags=re.DOTALL)
1120
+ text = re.sub(r"`[^`]+`", "", text)
1121
+ text = re.sub(
1122
+ r"^##\s+Do NOT\s*$.*?(?=^##\s+|\Z)",
1123
+ "", text, flags=re.MULTILINE | re.DOTALL,
1124
+ )
1125
+ text = re.sub(
1126
+ r"\*\*WHEN NOT to use this\*\*.*?(?=\*\*WHEN|^##\s+|\Z)",
1127
+ "", text, flags=re.DOTALL | re.IGNORECASE,
1128
+ )
1129
+ return text
1130
+
1131
+
1132
+ def lint_wing3_boundaries(text: str) -> List[Issue]:
1133
+ """Four Wing-3 GTM cognition-boundary checks.
1134
+
1135
+ Triggered when a skill's ``context_spine`` declares at least one
1136
+ Wing-3 slot (channel-stage, funnel-stage, customer-segment). Enforces
1137
+ council Q7 / iter-2 OQ3 verdict that GTM cognition stays:
1138
+
1139
+ - **agent-operability** — no external SaaS URLs the agent would auth against.
1140
+ - **vendor-independence** — no platform / SDK / brand slugs.
1141
+ - **transferability** — no stack-locked tooling instructions.
1142
+ - **channel-agnosticism** — no channel-specific tactical prescriptions.
1143
+
1144
+ Carve-outs: fenced code, inline backticks, the ``## Do NOT`` block,
1145
+ and ``**WHEN NOT to use this**`` lists — so authors can cite a vendor
1146
+ as off-scope without tripping the linter.
1147
+ """
1148
+ issues: List[Issue] = []
1149
+ body = _strip_wing3_carve_outs(text)
1150
+
1151
+ match = WING3_SAAS_URL_PATTERN.search(body)
1152
+ if match:
1153
+ issues.append(Issue(
1154
+ "warning", "wing3_agent_operability",
1155
+ f"Wing-3 skill cites external SaaS URL `{match.group(0)}` outside "
1156
+ f"carve-outs — cognition skills must operate without SaaS auth "
1157
+ f"(council Q7 boundary)",
1158
+ ))
1159
+
1160
+ match = WING3_VENDOR_BLACKLIST.search(body)
1161
+ if match:
1162
+ issues.append(Issue(
1163
+ "warning", "wing3_vendor_independence",
1164
+ f"Wing-3 skill names vendor `{match.group(0)}` outside carve-outs "
1165
+ f"— keep cognition vendor-agnostic (council Q7 boundary)",
1166
+ ))
1167
+
1168
+ match = WING3_STACK_LOCKED_PATTERN.search(body)
1169
+ if match:
1170
+ issues.append(Issue(
1171
+ "warning", "wing3_transferability",
1172
+ f"Wing-3 skill includes stack-locked instruction `{match.group(0)}` "
1173
+ f"outside carve-outs — cognition should transfer across stacks "
1174
+ f"(council Q7 boundary)",
1175
+ ))
1176
+
1177
+ match = WING3_CHANNEL_TACTIC_PATTERN.search(body)
1178
+ if match:
1179
+ issues.append(Issue(
1180
+ "warning", "wing3_channel_agnosticism",
1181
+ f"Wing-3 skill prescribes channel-specific tactic "
1182
+ f"`{match.group(0)}` outside carve-outs — keep cognition "
1183
+ f"channel-agnostic (council Q7 boundary)",
1184
+ ))
1185
+
1186
+ return issues
1187
+
1188
+
1189
+ def lint_wing4_boundaries(text: str) -> List[Issue]:
1190
+ """Four Wing-4 Money/Strategy/Ops cognition-boundary checks.
1191
+
1192
+ Triggered when a skill's ``context_spine`` declares at least one
1193
+ Wing-4 slot (fiscal-period, org-stage, regulatory-regime). Enforces
1194
+ council Q7 / J2 verdict that Money/Strategy/Ops cognition stays:
1195
+
1196
+ - **agent-operability** — no external finance/HR/legal SaaS URLs.
1197
+ - **vendor-independence** — no QuickBooks/Carta/Gusto-class brand slugs.
1198
+ - **transferability** — no stack-locked tooling instructions.
1199
+ - **stage-agnosticism** — no prescriptive stage-specific thresholds.
1200
+
1201
+ Carve-outs are identical to Wing-3: fenced code, inline backticks,
1202
+ the ``## Do NOT`` block, and ``**WHEN NOT to use this**`` lists.
1203
+ Regulatory regime names (GDPR / HIPAA / SOC2 / PCI / CCPA) are
1204
+ cognition-relevant constraints, not vendors — they pass.
1205
+ """
1206
+ issues: List[Issue] = []
1207
+ body = _strip_wing3_carve_outs(text)
1208
+
1209
+ match = WING4_SAAS_URL_PATTERN.search(body)
1210
+ if match:
1211
+ issues.append(Issue(
1212
+ "warning", "wing4_agent_operability",
1213
+ f"Wing-4 skill cites external SaaS URL `{match.group(0)}` outside "
1214
+ f"carve-outs — cognition skills must operate without SaaS auth "
1215
+ f"(council Q7 boundary)",
1216
+ ))
1217
+
1218
+ match = WING4_VENDOR_BLACKLIST.search(body)
1219
+ if match:
1220
+ issues.append(Issue(
1221
+ "warning", "wing4_vendor_independence",
1222
+ f"Wing-4 skill names vendor `{match.group(0)}` outside carve-outs "
1223
+ f"— keep cognition vendor-agnostic (council Q7 boundary)",
1224
+ ))
1225
+
1226
+ match = WING3_STACK_LOCKED_PATTERN.search(body)
1227
+ if match:
1228
+ issues.append(Issue(
1229
+ "warning", "wing4_transferability",
1230
+ f"Wing-4 skill includes stack-locked instruction `{match.group(0)}` "
1231
+ f"outside carve-outs — cognition should transfer across stacks "
1232
+ f"(council Q7 boundary)",
1233
+ ))
1234
+
1235
+ match = WING4_STAGE_AGNOSTIC_PATTERN.search(body)
1236
+ if match:
1237
+ issues.append(Issue(
1238
+ "warning", "wing4_stage_agnosticism",
1239
+ f"Wing-4 skill prescribes stage-locked threshold "
1240
+ f"`{match.group(0)}` outside carve-outs — cognition must "
1241
+ f"transfer across seed and public (council Q7 boundary)",
1242
+ ))
1243
+
1244
+ return issues
1245
+
1246
+
996
1247
  def lint_execution_metadata(execution: dict) -> List[Issue]:
997
1248
  """Validate the execution block of a skill."""
998
1249
  issues: List[Issue] = []
@@ -1436,6 +1687,27 @@ def lint_persona(path: Path, text: str) -> LintResult:
1436
1687
  f"Persona tier `{parsed['tier']}` must be one of {sorted(VALID_PERSONA_TIERS)}",
1437
1688
  ))
1438
1689
 
1690
+ # wing — optional; when present must be one of {1,2,3,4} (per
1691
+ # docs/contracts/package-self-orientation.md § The four wings).
1692
+ wing_match = re.search(r'^wing:\s*"?(\d+)"?\s*$', frontmatter, re.MULTILINE)
1693
+ if wing_match:
1694
+ try:
1695
+ wing_value = int(wing_match.group(1))
1696
+ if wing_value in VALID_PERSONA_WINGS:
1697
+ parsed["wing"] = wing_value
1698
+ else:
1699
+ issues.append(Issue(
1700
+ "error",
1701
+ "invalid_wing",
1702
+ f"Persona wing `{wing_value}` must be one of {sorted(VALID_PERSONA_WINGS)}",
1703
+ ))
1704
+ except ValueError:
1705
+ issues.append(Issue(
1706
+ "error",
1707
+ "invalid_wing",
1708
+ f"Persona wing `{wing_match.group(1)}` must be an integer 1–4",
1709
+ ))
1710
+
1439
1711
  # description length
1440
1712
  if "description" in parsed and len(parsed["description"]) > 160:
1441
1713
  issues.append(Issue(
@@ -1473,15 +1745,21 @@ def lint_persona(path: Path, text: str) -> LintResult:
1473
1745
  f"Persona has {bullet_count} unique questions (target ≥ 3)",
1474
1746
  ))
1475
1747
 
1476
- # Size budget by tier
1748
+ # Size budget by tier — wing-overrides apply when the persona declares a
1749
+ # `wing:` field; defaults to the tier baseline otherwise.
1477
1750
  if "tier" in parsed and parsed["tier"] in PERSONA_LINE_BUDGETS:
1478
- budget = PERSONA_LINE_BUDGETS[parsed["tier"]]
1751
+ tier_value = parsed["tier"]
1752
+ wing_value = parsed.get("wing")
1753
+ budget = PERSONA_LINE_BUDGETS_BY_WING.get(
1754
+ (tier_value, wing_value), PERSONA_LINE_BUDGETS[tier_value]
1755
+ )
1479
1756
  line_count = len(text.splitlines())
1480
1757
  if line_count > budget:
1758
+ scope = f"{tier_value}" if wing_value is None else f"{tier_value}, wing {wing_value}"
1481
1759
  issues.append(Issue(
1482
1760
  "warning",
1483
1761
  "size_budget",
1484
- f"Persona has {line_count} lines ({parsed['tier']} budget ≤ {budget})",
1762
+ f"Persona has {line_count} lines ({scope} budget ≤ {budget})",
1485
1763
  ))
1486
1764
 
1487
1765
  # H1 heading