create-workframe 0.1.5 → 0.1.7
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/.dockerignore +22 -22
- package/.gitignore +73 -73
- package/LICENSE +201 -201
- package/NOTICE +12 -12
- package/README.md +40 -40
- package/SECURITY.md +38 -38
- package/bin/create-workframe.js +2816 -2814
- package/bin/workframe.js +329 -329
- package/docs/workspace-instructions/WORKFRAME_DISCORD.md +20 -20
- package/docs/workspace-instructions/WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md +20 -20
- package/docs/workspace-instructions/WORKFRAME_KANBAN.md +20 -20
- package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +21 -21
- package/docs/workspace-instructions/WORKFRAME_ROUTING.md +29 -29
- package/docs/workspace-instructions/WORKFRAME_TELEGRAM.md +19 -19
- package/package.json +64 -64
- package/profiles/README.md +15 -15
- package/profiles/architect/AGENTS.md +29 -29
- package/profiles/architect/SOUL.md +44 -44
- package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/designer/AGENTS.md +26 -26
- package/profiles/designer/SOUL.md +31 -31
- package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/dev/AGENTS.md +28 -28
- package/profiles/dev/SOUL.md +31 -31
- package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/docs/AGENTS.md +27 -27
- package/profiles/docs/SOUL.md +30 -30
- package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/research/AGENTS.md +26 -26
- package/profiles/research/SOUL.md +31 -31
- package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/visionary/AGENTS.md +25 -25
- package/profiles/visionary/SOUL.md +31 -31
- package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
- package/profiles/workframe-agent/AGENTS.md +37 -37
- package/profiles/workframe-agent/SETUP.md +185 -185
- package/profiles/workframe-agent/SOUL.md +61 -61
- package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
- package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
- package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
- package/prompts/WORKFRAME_PROMPT_TEMPLATES.md +16 -16
- package/rules/.hermes.md +11 -11
- package/rules/AGENTS.md +22 -22
- package/rules/workspace-README.md +5 -5
- package/scripts/apply-update-workframe.sh +91 -21
- package/scripts/bundle-workframe-ui.mjs +83 -83
- package/scripts/create_workframe_scaffold.py +648 -648
- package/scripts/ensure-compose-host-paths.mjs +51 -51
- package/scripts/lib/install-identity.mjs +212 -212
- package/scripts/lib/workframe-registry.mjs +290 -290
- package/scripts/new-project.mjs +68 -68
- package/scripts/select_agent_pack.py +31 -31
- package/scripts/set-compose-public-url.mjs +92 -92
- package/scripts/sync-canonical-to-package.mjs +164 -164
- package/shared/WORKFRAME_AGENT_LIBRARY.md +31 -31
- package/shared/WORKFRAME_AGENT_OPERATIONS.md +29 -29
- package/shared/WORKFRAME_AGENT_PACKS.json +64 -64
- package/shared/WORKFRAME_AGENT_PACKS.yaml +20 -20
- package/shared/WORKFRAME_CHAT_PERMISSION_MODEL.md +20 -20
- package/shared/WORKFRAME_HANDOFF_SCHEMA.md +25 -25
- package/shared/WORKFRAME_SKILL_CURATION.md +27 -27
- package/workframe-api/Dockerfile +14 -14
- package/workframe-api/README.md +24 -24
- package/workframe-api/action_proxy.py +131 -131
- package/workframe-api/auth_rate_limit.py +49 -49
- package/workframe-api/credential_vault.py +445 -445
- package/workframe-api/data/avatar-catalog.json +41 -41
- package/workframe-api/data/logo-catalog.json +14 -14
- package/workframe-api/data/user-avatar-catalog.json +18 -18
- package/workframe-api/email_sender.py +220 -220
- package/workframe-api/google_auth.py +90 -90
- package/workframe-api/install_api.py +359 -359
- package/workframe-api/internal_proxy_auth.py +150 -150
- package/workframe-api/llm_proxy.py +280 -280
- package/workframe-api/oidc_jwt.py +108 -108
- package/workframe-api/package.json +12 -12
- package/workframe-api/platform_auth.py +194 -194
- package/workframe-api/profile_secret_policy.py +86 -86
- package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
- package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
- package/workframe-api/public/assets/index-DglUqFB_.js +8 -8
- package/workframe-api/public/index.html +12 -12
- package/workframe-api/requirements.txt +2 -2
- package/workframe-api/server.py +20295 -19866
- package/workframe-api/site_meta.py +271 -271
- package/workframe-api/stack_config.py +435 -427
- package/workframe-api/test_ensure_native_profile.py +29 -0
- package/workframe-api/test_oauth_llm.py +58 -0
- package/workframe-api/test_provider_model_surface.py +31 -0
- package/workframe-api/test_stack_config_install_smtp.py +51 -0
- package/workframe-api/time-bind-chat.py +99 -99
- package/workframe-api/turn_credentials.py +226 -226
- package/workframe-api/updates.py +506 -506
- package/workframe-api/vault_kek.py +159 -159
- package/workframe-api/zk_auth.py +633 -633
- package/workframe-supervisor/Dockerfile +11 -11
- package/workframe-supervisor/profile_secret_policy.py +76 -76
- package/workframe-supervisor/server.py +860 -860
- package/workframe-ui/docker/nginx.conf +85 -85
- package/workframe-ui/public/assets/{arc-CnriPN3G.js → arc-Cn-lt9fT.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CCMNXHod.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DYYB1Ic2.js → architectureDiagram-3BPJPVTR-qXoGCqfH.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-Bn-fFOyj.js → blockDiagram-GPEHLZMM-DoopFlWj.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BPPDtAnD.js → c4Diagram-AAUBKEIU-BRa7hfzE.js} +1 -1
- package/workframe-ui/public/assets/channel-CtFV3sE3.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Bz2CJiC3.js → chunk-2J33WTMH-Cl33E2hh.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-B8aMeE1B.js → chunk-3OPIFGDE-BRywYZQO.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-bLFHQTuT.js → chunk-4BX2VUAB-CI3xzp-0.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-BRxhx-lG.js → chunk-55IACEB6-Bkf3Ye3F.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BLyOgEJ8.js → chunk-5ZQYHXKU-C5RN3hUT.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-DiuvBl7G.js → chunk-727SXJPM-BaY0yDwt.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-BE1_aBdC.js → chunk-AQP2D5EJ-VMMBaDqd.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-BicGsmBs.js → chunk-BSJP7CBP-DlNZ1UV3.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-C3VTns3d.js → chunk-CSCIHK7Q-tT2HyVy0.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-DSiXY3bY.js → chunk-FMBD7UC4-CyACRaUT.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-Cvw5s3fR.js → chunk-KSCS5N6A-seK5WAZs.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-jJifGdIQ.js → chunk-L5ZTLDWV-r5cyG5AH.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-Maee1aqo.js → chunk-LZXEDZCA-SnbeVxsJ.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-C77N3VAM.js → chunk-ND2GUHAM-zbFgEXSJ.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-BY68lrPB.js → chunk-NZK2D7GU-UhZc8kOk.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-D_c4kWzh.js → chunk-O5CBEL6O-DHvu2iZ-.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-B1g7l1_B.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-B_qfZ6QP.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-DlQgvqBp.js → chunk-XPW4576I-CCPNKbzG.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CJoeCNq6.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CJoeCNq6.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CHCOHDSd.js → cose-bilkent-S5V4N54A-D1CLe6Y0.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-CKPcSYwU.js → dagre-BM42HDAG-DYHcGVs_.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-C6qe0jP-.js → diagram-2AECGRRQ-DVUkJ-II.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-DhQhrieU.js → diagram-5GNKFQAL-C9Y1Acc2.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Bqf6Iw1r.js → diagram-KO2AKTUF-Db3zez-n.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt1ms2D2.js → diagram-LMA3HP47-BaOR5sIr.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-C0Rrxe1i.js → diagram-OG6HWLK6-fLBzMY5W.js} +1 -1
- package/workframe-ui/public/assets/{dist-BLcLhTn6.js → dist-uZYeD3o1.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-Cy1GbZVu.js → erDiagram-TEJ5UH35-BoCxDSyE.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-Bw910jho.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-DL3ALqDG.js → flowDiagram-I6XJVG4X-BnYa3CAj.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-CMpwBhXQ.js → ganttDiagram-6RSMTGT7-Dm_e_Kaa.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-C0mwHUXL.js → gitGraph-WXDBUCRP-CBAj8ntv.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-6gzhWaF0.js → gitGraphDiagram-PVQCEYII-D5auJjJh.js} +1 -1
- package/workframe-ui/public/assets/index-DHLQNAXd.js +129 -0
- package/workframe-ui/public/assets/index-auJ0bLMU.css +1 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-Bb_wZTxU.js → info-J43DQDTF-BNMADxE2.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-601evYFV.js → infoDiagram-5YYISTIA-C8c45T4J.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BYtF-wHm.js → ishikawaDiagram-YF4QCWOH-CyWuHB4e.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-BMizyHzC.js → journeyDiagram-JHISSGLW-B43pCuDT.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CLLJsdnh.js → kanban-definition-UN3LZRKU-Dyn480BI.js} +1 -1
- package/workframe-ui/public/assets/{line-BFiSvu-q.js → line-CNtIbnmt.js} +1 -1
- package/workframe-ui/public/assets/{linear-D6HYZ18_.js → linear-pmSg_4uV.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Cid5yDBQ.js → mermaid-parser.core-BX4tnojE.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-DqIiju5Z.js → mermaid.core-BtpBva5o.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-Bg7xJdsz.js → mindmap-definition-RKZ34NQL-DcMcB_69.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-1ebGt86D.js → packet-YPE3B663-KSzK9WlD.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DJ9wOBC_.js → pie-LRSECV5Y-DLOH6Mg8.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-BmK96Aus.js → pieDiagram-4H26LBE5-BhrX2aGk.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-ByUqtWPZ.js → quadrantDiagram-W4KKPZXB-B5dDjbTX.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-DN-Hkt96.js → radar-GUYGQ44K-BPdjqZiu.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ByU2r8gb.js → requirementDiagram-4Y6WPE33-21Dq5e0N.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-elyWa0GK.js → sankeyDiagram-5OEKKPKP-CiA99dbO.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-CzZwsAiK.js → sequenceDiagram-3UESZ5HK-Bwpui7_x.js} +1 -1
- package/workframe-ui/public/assets/{src-5vgsM_dl.js → src-B3Axsi1U.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-B0O5w5lJ.js → stateDiagram-AJRCARHV-C76qwtE8.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-Dn0CDWJD.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-oyqa5w5y.js → timeline-definition-PNZ67QCA-CufInOyk.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-MCV12-eP.js → treeView-BLDUP644-C4sxh0R-.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-DiA1P9n5.js → treemap-LRROVOQU-BjV72ANV.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CO_4x7OR.js → vennDiagram-CIIHVFJN-DgsdR0Rl.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-CmQNv6Xv.js → wardley-L42UT6IY-DZ279G_t.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-CJxDkbIj.js → wardleyDiagram-YWT4CUSO-Bde25kvz.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-B02MxgzG.js → xychartDiagram-2RQKCTM6-B8VnS2Pb.js} +1 -1
- package/workframe-ui/public/favicon.svg +7 -7
- package/workframe-ui/public/icons.svg +24 -24
- package/workframe-ui/public/index.html +47 -47
- package/workframe-ui/public/manifest.webmanifest +18 -18
- package/workframe-ui/public/workframe-config.json +3 -3
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CFhr7xAe.js +0 -1
- package/workframe-ui/public/assets/channel-Dok_-T0o.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-C3wwLrTa.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-GFFSg2kf.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DKbGfT5Z.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DKbGfT5Z.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-C_PrceHe.js +0 -1
- package/workframe-ui/public/assets/index-DyO6K0TD.css +0 -1
- package/workframe-ui/public/assets/index-yvpSUs8T.js +0 -133
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-RXZRaSgT.js +0 -1
|
@@ -1,648 +1,648 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""DEV/CI helper — NOT the canonical installer.
|
|
3
|
-
|
|
4
|
-
Canonical scaffold: bin/create-workframe.js (npm / npx create-workframe).
|
|
5
|
-
This script exists for Python-only smoke tests and security audit workflows.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import argparse
|
|
11
|
-
import json
|
|
12
|
-
import re
|
|
13
|
-
import shutil
|
|
14
|
-
from datetime import datetime, timezone
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
18
|
-
PKG_ROOT = SCRIPT_DIR.parent
|
|
19
|
-
PACKS_JSON = PKG_ROOT / 'shared' / 'WORKFRAME_AGENT_PACKS.json'
|
|
20
|
-
WORKSPACE_DOCS = PKG_ROOT / 'docs' / 'workspace-instructions'
|
|
21
|
-
|
|
22
|
-
PROJECT_AGENT_SLOT = 'project-agent'
|
|
23
|
-
NATIVE_SOUL_TEMPLATE = 'workframe-agent'
|
|
24
|
-
|
|
25
|
-
PROFILE_DESCRIPTIONS = {
|
|
26
|
-
'visionary': 'Clarifies product purpose, positioning, strategy, user value, and long-term alignment.',
|
|
27
|
-
'architect': 'Defines system design, technical boundaries, implementation plans, and code-review standards.',
|
|
28
|
-
'docs': 'Maintains AGENTS.md, .hermes.md, docs indexes, source-of-truth maps, and change summaries.',
|
|
29
|
-
'dev': 'Builds and modifies project files, scripts, tests, and implementation artifacts.',
|
|
30
|
-
'research': 'Performs technical research, market research, references, competitive analysis, and R&D notes.',
|
|
31
|
-
'designer': 'Handles UI direction, design docs, visual assets, image prompts, brand direction, and layout feedback.',
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
SHARED_DOCS = ['WORKFRAME_AGENT_LIBRARY.md', 'WORKFRAME_HANDOFF_SCHEMA.md']
|
|
35
|
-
|
|
36
|
-
GITIGNORE = """# Runtime state: do not commit instance data
|
|
37
|
-
Agents/
|
|
38
|
-
**/Agents/
|
|
39
|
-
*.db
|
|
40
|
-
*.db-shm
|
|
41
|
-
*.db-wal
|
|
42
|
-
*.log
|
|
43
|
-
logs/
|
|
44
|
-
cache/
|
|
45
|
-
memories/
|
|
46
|
-
sessions/
|
|
47
|
-
kanban/
|
|
48
|
-
state/
|
|
49
|
-
|
|
50
|
-
# Bootstrap seed (optional cleanup after profile bootstrap)
|
|
51
|
-
scripts/seed/
|
|
52
|
-
|
|
53
|
-
# Secrets
|
|
54
|
-
.env
|
|
55
|
-
.env.local
|
|
56
|
-
.env.*.local
|
|
57
|
-
*.pem
|
|
58
|
-
*.key
|
|
59
|
-
*.p12
|
|
60
|
-
*.pfx
|
|
61
|
-
secrets/
|
|
62
|
-
|
|
63
|
-
# Build/tool noise
|
|
64
|
-
node_modules/
|
|
65
|
-
.venv/
|
|
66
|
-
__pycache__/
|
|
67
|
-
.pytest_cache/
|
|
68
|
-
.DS_Store
|
|
69
|
-
Thumbs.db
|
|
70
|
-
.vscode/
|
|
71
|
-
.idea/
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
DOCKERIGNORE = """.git
|
|
75
|
-
.gitignore
|
|
76
|
-
node_modules
|
|
77
|
-
.venv
|
|
78
|
-
__pycache__
|
|
79
|
-
.pytest_cache
|
|
80
|
-
*.pyc
|
|
81
|
-
*.pyo
|
|
82
|
-
*.db
|
|
83
|
-
*.db-shm
|
|
84
|
-
*.db-wal
|
|
85
|
-
*.log
|
|
86
|
-
.env
|
|
87
|
-
.env.*
|
|
88
|
-
Agents
|
|
89
|
-
cache
|
|
90
|
-
logs
|
|
91
|
-
memories
|
|
92
|
-
sessions
|
|
93
|
-
kanban
|
|
94
|
-
state
|
|
95
|
-
scripts/seed
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def slugify(name: str) -> str:
|
|
100
|
-
slug = re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-')
|
|
101
|
-
return slug or 'workframe'
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def native_profile_slug(project_name: str) -> str:
|
|
105
|
-
return f'{slugify(project_name)}-agent'
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def native_agent_name(project_name: str) -> str:
|
|
109
|
-
return f'{project_name} Agent'
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def render_context(project_name: str) -> dict[str, str]:
|
|
113
|
-
return {
|
|
114
|
-
'projectName': project_name,
|
|
115
|
-
'nativeProfileSlug': native_profile_slug(project_name),
|
|
116
|
-
'nativeAgentName': native_agent_name(project_name),
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def render_placeholders(text: str, ctx: dict[str, str] | str) -> str:
|
|
121
|
-
context = render_context(ctx) if isinstance(ctx, str) else ctx
|
|
122
|
-
out = text
|
|
123
|
-
for key, value in context.items():
|
|
124
|
-
out = out.replace(f'{{{key}}}', value)
|
|
125
|
-
return out
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def resolve_pack_profiles(base_profiles: list[str], project_name: str) -> list[str]:
|
|
129
|
-
native = native_profile_slug(project_name)
|
|
130
|
-
seen: set[str] = set()
|
|
131
|
-
resolved: list[str] = []
|
|
132
|
-
for profile in base_profiles:
|
|
133
|
-
slug = native if profile == PROJECT_AGENT_SLOT else profile
|
|
134
|
-
if slug not in seen:
|
|
135
|
-
seen.add(slug)
|
|
136
|
-
resolved.append(slug)
|
|
137
|
-
return resolved
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def profile_description(profile: str, project_name: str) -> str:
|
|
141
|
-
if profile == native_profile_slug(project_name):
|
|
142
|
-
return (
|
|
143
|
-
f'{native_agent_name(project_name)}: host, concierge, project manager, '
|
|
144
|
-
'orchestrator, and Workframe admin.'
|
|
145
|
-
)
|
|
146
|
-
return PROFILE_DESCRIPTIONS.get(profile, f'{profile} specialist profile.')
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def profile_soul_source(profile: str, project_name: str) -> Path:
|
|
150
|
-
template_dir = NATIVE_SOUL_TEMPLATE if profile == native_profile_slug(project_name) else profile
|
|
151
|
-
return PKG_ROOT / 'profiles' / template_dir / 'SOUL.md'
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def docker_container_names(project_name: str) -> dict:
|
|
155
|
-
slug = slugify(project_name)
|
|
156
|
-
stack = slug
|
|
157
|
-
return {
|
|
158
|
-
'slug': slug,
|
|
159
|
-
'stack': stack,
|
|
160
|
-
'image': 'nousresearch/hermes-agent:latest',
|
|
161
|
-
'network': f'{stack}-net',
|
|
162
|
-
'gateway': f'{stack}-gateway',
|
|
163
|
-
'dashboard': f'{stack}-dashboard',
|
|
164
|
-
'workframe_api': f'{stack}-workframe-api',
|
|
165
|
-
'workframe': f'{stack}-workframe',
|
|
166
|
-
'chat': f'{stack}-chat',
|
|
167
|
-
'setup': f'{stack}-setup',
|
|
168
|
-
'bootstrap_use': f'{stack}-bootstrap-use',
|
|
169
|
-
'bootstrap_list': f'{stack}-bootstrap-list',
|
|
170
|
-
'profile_dashboard': lambda profile: f'{stack}-dashboard-{profile}',
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def hermes_service_volumes_block() -> str:
|
|
175
|
-
return """ volumes:
|
|
176
|
-
- ./Agents:/opt/data
|
|
177
|
-
- ./Files:/workspace
|
|
178
|
-
- ./scripts:/opt/install/scripts:ro"""
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def profile_dashboard_service_yaml(profile: str, docker: dict, label_project: str, network: str) -> str:
|
|
182
|
-
profile_esc = profile.replace('"', '\\"')
|
|
183
|
-
return f"""
|
|
184
|
-
dashboard-{profile}:
|
|
185
|
-
image: {docker['image']}
|
|
186
|
-
container_name: {docker['profile_dashboard'](profile)}
|
|
187
|
-
restart: unless-stopped
|
|
188
|
-
command: ["hermes", "-p", "{profile_esc}", "dashboard", "--host", "0.0.0.0", "--insecure", "--tui"]
|
|
189
|
-
labels:
|
|
190
|
-
com.workframe.project: "{label_project}"
|
|
191
|
-
com.workframe.role: profile-dashboard
|
|
192
|
-
com.workframe.profile: "{profile_esc}"
|
|
193
|
-
expose:
|
|
194
|
-
- "9119"
|
|
195
|
-
{hermes_service_volumes_block()}
|
|
196
|
-
environment:
|
|
197
|
-
- GATEWAY_HEALTH_URL=http://gateway:8642
|
|
198
|
-
- HERMES_DASHBOARD_TUI=1
|
|
199
|
-
depends_on:
|
|
200
|
-
- gateway
|
|
201
|
-
networks:
|
|
202
|
-
- {network}"""
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def docker_compose_yaml(project_name: str, docker: dict, native_profile: str, _pack_profiles: list[str]) -> str:
|
|
206
|
-
label_project = project_name.replace('\\', '\\\\').replace('"', '\\"')
|
|
207
|
-
profile_esc = native_profile.replace('"', '\\"')
|
|
208
|
-
dashboard_profiles = [native_profile]
|
|
209
|
-
profile_services = ''.join(
|
|
210
|
-
profile_dashboard_service_yaml(p, docker, label_project, docker['network']) for p in dashboard_profiles
|
|
211
|
-
)
|
|
212
|
-
profile_depends = ''.join(f'\n - dashboard-{p}' for p in pack_profiles)
|
|
213
|
-
volumes = hermes_service_volumes_block()
|
|
214
|
-
return f"""name: {docker['stack']}
|
|
215
|
-
|
|
216
|
-
services:
|
|
217
|
-
gateway:
|
|
218
|
-
image: {docker['image']}
|
|
219
|
-
container_name: {docker['gateway']}
|
|
220
|
-
restart: unless-stopped
|
|
221
|
-
command: ["hermes", "-p", "{profile_esc}", "gateway", "run"]
|
|
222
|
-
labels:
|
|
223
|
-
com.workframe.project: "{label_project}"
|
|
224
|
-
com.workframe.role: gateway
|
|
225
|
-
ports:
|
|
226
|
-
- "127.0.0.1:18642:8642"
|
|
227
|
-
{volumes}
|
|
228
|
-
networks:
|
|
229
|
-
- {docker['network']}
|
|
230
|
-
|
|
231
|
-
dashboard:
|
|
232
|
-
image: {docker['image']}
|
|
233
|
-
container_name: {docker['dashboard']}
|
|
234
|
-
restart: unless-stopped
|
|
235
|
-
command: dashboard --host 0.0.0.0 --insecure --tui
|
|
236
|
-
labels:
|
|
237
|
-
com.workframe.project: "{label_project}"
|
|
238
|
-
com.workframe.role: dashboard
|
|
239
|
-
ports:
|
|
240
|
-
- "127.0.0.1:19119:9119"
|
|
241
|
-
{volumes}
|
|
242
|
-
environment:
|
|
243
|
-
- GATEWAY_HEALTH_URL=http://gateway:8642
|
|
244
|
-
- HERMES_DASHBOARD_TUI=1
|
|
245
|
-
depends_on:
|
|
246
|
-
- gateway
|
|
247
|
-
networks:
|
|
248
|
-
- {docker['network']}
|
|
249
|
-
{profile_services}
|
|
250
|
-
|
|
251
|
-
networks:
|
|
252
|
-
{docker['network']}:
|
|
253
|
-
driver: bridge
|
|
254
|
-
"""
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def setup_sh() -> str:
|
|
258
|
-
return """#!/usr/bin/env bash
|
|
259
|
-
set -euo pipefail
|
|
260
|
-
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
261
|
-
cd "$ROOT"
|
|
262
|
-
|
|
263
|
-
mkdir -p Agents Files
|
|
264
|
-
|
|
265
|
-
echo "Pulling Hermes image..."
|
|
266
|
-
docker pull nousresearch/hermes-agent:latest
|
|
267
|
-
|
|
268
|
-
echo ""
|
|
269
|
-
echo "Run Hermes setup (interactive):"
|
|
270
|
-
echo " docker run --rm -it --entrypoint hermes \\\\"
|
|
271
|
-
echo " -v \\"\\$PWD/Agents:/opt/data\\" \\\\"
|
|
272
|
-
echo " -v \\"\\$PWD/Files:/workspace\\" \\\\"
|
|
273
|
-
echo " nousresearch/hermes-agent:latest setup"
|
|
274
|
-
echo ""
|
|
275
|
-
echo "Then bootstrap profiles:"
|
|
276
|
-
echo " ./scripts/bootstrap-profiles.sh"
|
|
277
|
-
echo ""
|
|
278
|
-
echo "Start gateway + dashboard:"
|
|
279
|
-
echo " docker compose up -d"
|
|
280
|
-
"""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def setup_ps1() -> str:
|
|
284
|
-
return """$ErrorActionPreference = 'Stop'
|
|
285
|
-
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|
286
|
-
Set-Location $Root
|
|
287
|
-
|
|
288
|
-
New-Item -ItemType Directory -Force -Path Agents, Files | Out-Null
|
|
289
|
-
|
|
290
|
-
Write-Host 'Pulling Hermes image...'
|
|
291
|
-
docker pull nousresearch/hermes-agent:latest
|
|
292
|
-
|
|
293
|
-
Write-Host ''
|
|
294
|
-
Write-Host 'Run Hermes setup (interactive):'
|
|
295
|
-
Write-Host ' docker run --rm -it --entrypoint hermes `'
|
|
296
|
-
Write-Host ' -v "$PWD\\Agents:/opt/data" `'
|
|
297
|
-
Write-Host ' -v "$PWD\\Files:/workspace" `'
|
|
298
|
-
Write-Host ' nousresearch/hermes-agent:latest setup'
|
|
299
|
-
Write-Host ''
|
|
300
|
-
Write-Host 'Then bootstrap profiles:'
|
|
301
|
-
Write-Host ' .\\scripts\\bootstrap-profiles.ps1'
|
|
302
|
-
Write-Host ''
|
|
303
|
-
Write-Host 'Start gateway + dashboard:'
|
|
304
|
-
Write-Host ' docker compose up -d'
|
|
305
|
-
"""
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def bootstrap_profiles_sh(profiles: list[str], project_name: str, native_profile: str, docker: dict) -> str:
|
|
309
|
-
blocks = []
|
|
310
|
-
for p in profiles:
|
|
311
|
-
desc = profile_description(p, project_name).replace('"', '\\"')
|
|
312
|
-
create_name = f"{docker['stack']}-bootstrap-{p}"
|
|
313
|
-
show_name = f"{create_name}-show"
|
|
314
|
-
blocks.append(f"""echo "Creating profile: {p}"
|
|
315
|
-
docker run --rm --name "{create_name}" --entrypoint hermes \\
|
|
316
|
-
-v "$ROOT/Agents:/opt/data" \\
|
|
317
|
-
-v "$ROOT/Files:/workspace" \\
|
|
318
|
-
{docker['image']} profile create {p} --clone --description "{desc}" || true
|
|
319
|
-
|
|
320
|
-
if [ -f "$ROOT/scripts/seed/profiles/{p}/SOUL.md" ]; then
|
|
321
|
-
mkdir -p "$ROOT/Agents/profiles/{p}"
|
|
322
|
-
cp "$ROOT/scripts/seed/profiles/{p}/SOUL.md" "$ROOT/Agents/profiles/{p}/SOUL.md"
|
|
323
|
-
fi
|
|
324
|
-
""")
|
|
325
|
-
body = '\n'.join(blocks)
|
|
326
|
-
return f"""#!/usr/bin/env bash
|
|
327
|
-
set -euo pipefail
|
|
328
|
-
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
329
|
-
cd "$ROOT"
|
|
330
|
-
|
|
331
|
-
if [ ! -d "$ROOT/Agents" ]; then
|
|
332
|
-
echo "Agents/ missing. Run Hermes setup first."
|
|
333
|
-
exit 1
|
|
334
|
-
fi
|
|
335
|
-
|
|
336
|
-
{body}
|
|
337
|
-
echo "Setting default profile to {native_profile}..."
|
|
338
|
-
docker run --rm --name "{docker['bootstrap_use']}" --entrypoint hermes \\
|
|
339
|
-
-v "$ROOT/Agents:/opt/data" \\
|
|
340
|
-
-v "$ROOT/Files:/workspace" \\
|
|
341
|
-
{docker['image']} profile use {native_profile}
|
|
342
|
-
|
|
343
|
-
echo "Profile bootstrap complete."
|
|
344
|
-
docker run --rm --name "{docker['bootstrap_list']}" --entrypoint hermes \\
|
|
345
|
-
-v "$ROOT/Agents:/opt/data" \\
|
|
346
|
-
-v "$ROOT/Files:/workspace" \\
|
|
347
|
-
{docker['image']} profile list
|
|
348
|
-
"""
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def bootstrap_profiles_ps1(profiles: list[str], project_name: str, native_profile: str, docker: dict) -> str:
|
|
352
|
-
blocks = []
|
|
353
|
-
for p in profiles:
|
|
354
|
-
desc = profile_description(p, project_name).replace('"', '`"')
|
|
355
|
-
create_name = f"{docker['stack']}-bootstrap-{p}"
|
|
356
|
-
blocks.append(f"""Write-Host "Creating profile: {p}"
|
|
357
|
-
docker run --rm --name "{create_name}" --entrypoint hermes `
|
|
358
|
-
-v "$Root\\Agents:/opt/data" `
|
|
359
|
-
-v "$Root\\Files:/workspace" `
|
|
360
|
-
{docker['image']} profile create {p} --clone --description "{desc}" 2>$null
|
|
361
|
-
|
|
362
|
-
$seed = Join-Path $Root "scripts\\seed\\profiles\\{p}\\SOUL.md"
|
|
363
|
-
$destDir = Join-Path $Root "Agents\\profiles\\{p}"
|
|
364
|
-
if (Test-Path $seed) {{
|
|
365
|
-
New-Item -ItemType Directory -Force -Path $destDir | Out-Null
|
|
366
|
-
Copy-Item $seed (Join-Path $destDir "SOUL.md") -Force
|
|
367
|
-
}}
|
|
368
|
-
""")
|
|
369
|
-
body = '\n'.join(blocks)
|
|
370
|
-
return f"""$ErrorActionPreference = 'Stop'
|
|
371
|
-
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|
372
|
-
Set-Location $Root
|
|
373
|
-
|
|
374
|
-
if (-not (Test-Path "$Root\\Agents")) {{
|
|
375
|
-
throw "Agents/ missing. Run Hermes setup first."
|
|
376
|
-
}}
|
|
377
|
-
|
|
378
|
-
{body}
|
|
379
|
-
Write-Host "Setting default profile to {native_profile}..."
|
|
380
|
-
docker run --rm --name "{docker['bootstrap_use']}" --entrypoint hermes `
|
|
381
|
-
-v "$Root\\Agents:/opt/data" `
|
|
382
|
-
-v "$Root\\Files:/workspace" `
|
|
383
|
-
{docker['image']} profile use {native_profile}
|
|
384
|
-
|
|
385
|
-
Write-Host "Profile bootstrap complete."
|
|
386
|
-
docker run --rm --name "{docker['bootstrap_list']}" --entrypoint hermes `
|
|
387
|
-
-v "$Root\\Agents:/opt/data" `
|
|
388
|
-
-v "$Root\\Files:/workspace" `
|
|
389
|
-
{docker['image']} profile list
|
|
390
|
-
"""
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
CI_WORKFLOW = """name: workframe-security
|
|
394
|
-
|
|
395
|
-
on:
|
|
396
|
-
push:
|
|
397
|
-
pull_request:
|
|
398
|
-
|
|
399
|
-
jobs:
|
|
400
|
-
security-and-scaffold:
|
|
401
|
-
runs-on: ubuntu-latest
|
|
402
|
-
steps:
|
|
403
|
-
- uses: actions/checkout@v4
|
|
404
|
-
- uses: actions/setup-python@v5
|
|
405
|
-
with:
|
|
406
|
-
python-version: '3.12'
|
|
407
|
-
- uses: actions/setup-node@v4
|
|
408
|
-
with:
|
|
409
|
-
node-version: '20'
|
|
410
|
-
|
|
411
|
-
- name: Run security audit
|
|
412
|
-
run: python3 scripts/security_audit.py
|
|
413
|
-
working-directory: packages/create-workframe
|
|
414
|
-
|
|
415
|
-
- name: Generate scaffold with Python
|
|
416
|
-
run: python3 scripts/create_workframe_scaffold.py CiDemo --pack core --output /tmp --force
|
|
417
|
-
working-directory: packages/create-workframe
|
|
418
|
-
|
|
419
|
-
- name: Generate scaffold with Node installer
|
|
420
|
-
run: node bin/create-workframe.js --name CiNodeDemo --pack core --out /tmp --ci --force
|
|
421
|
-
working-directory: packages/create-workframe
|
|
422
|
-
|
|
423
|
-
- name: Verify generated layout
|
|
424
|
-
run: |
|
|
425
|
-
test -f /tmp/CiDemo/Files/AGENTS.md
|
|
426
|
-
test -f /tmp/CiDemo/docker-compose.yml
|
|
427
|
-
test -f /tmp/CiNodeDemo/Files/docs/SETUP.md
|
|
428
|
-
test -f /tmp/CiNodeDemo/scripts/bootstrap-profiles.sh
|
|
429
|
-
"""
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
def load_packs(path: Path) -> dict:
|
|
433
|
-
data = json.loads(path.read_text())
|
|
434
|
-
packs = data.get('packs', {})
|
|
435
|
-
if not packs:
|
|
436
|
-
raise SystemExit(f'No packs found in {path}')
|
|
437
|
-
return packs
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
def copy_tree(src: Path, dst: Path) -> None:
|
|
441
|
-
if not src.exists():
|
|
442
|
-
return
|
|
443
|
-
for p in src.rglob('*'):
|
|
444
|
-
if p.is_file():
|
|
445
|
-
rel = p.relative_to(src)
|
|
446
|
-
out = dst / rel
|
|
447
|
-
out.parent.mkdir(parents=True, exist_ok=True)
|
|
448
|
-
shutil.copy2(p, out)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
def make_setup_doc(project_name: str, pack: str, profiles: list[str], telegram: bool, discord: bool) -> str:
|
|
452
|
-
tg = 'Enabled in plan (optional integration chosen).' if telegram else 'Skipped (can enable later).'
|
|
453
|
-
dc = 'Enabled in plan (optional integration chosen).' if discord else 'Skipped (can enable later).'
|
|
454
|
-
return (
|
|
455
|
-
f"# {project_name} setup\n\n"
|
|
456
|
-
"## 1) Hermes base\n"
|
|
457
|
-
"- Run `./scripts/setup.sh` or `./scripts/setup.ps1`\n"
|
|
458
|
-
"- Then Hermes setup (interactive):\n"
|
|
459
|
-
"```bash\n"
|
|
460
|
-
"docker run --rm -it --entrypoint hermes \\\n"
|
|
461
|
-
' -v "${PWD}/Agents:/opt/data" \\\n'
|
|
462
|
-
' -v "${PWD}/Files:/workspace" \\\n'
|
|
463
|
-
" nousresearch/hermes-agent:latest setup\n"
|
|
464
|
-
"```\n\n"
|
|
465
|
-
"## 2) Bootstrap agent profiles\n"
|
|
466
|
-
"- `./scripts/bootstrap-profiles.sh` or `./scripts/bootstrap-profiles.ps1`\n\n"
|
|
467
|
-
"## 3) API/model keys\n"
|
|
468
|
-
"- `hermes auth add <provider>` at runtime\n\n"
|
|
469
|
-
"## 4) Start gateway + dashboard\n"
|
|
470
|
-
"- `docker compose up -d`\n\n"
|
|
471
|
-
"## 5) Chat integrations (optional)\n"
|
|
472
|
-
f"- Telegram: {tg}\n"
|
|
473
|
-
f"- Discord: {dc}\n\n"
|
|
474
|
-
"## 6) Agent pack\n"
|
|
475
|
-
f"- Pack: **{pack}**\n"
|
|
476
|
-
f"- Profiles: {', '.join(profiles)}\n"
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
def docs_index() -> str:
|
|
481
|
-
return (
|
|
482
|
-
"# Docs Index\n\n"
|
|
483
|
-
"- SETUP.md — first-run onboarding\n"
|
|
484
|
-
"- WORKFRAME_ONBOARDING.md — concierge loop\n"
|
|
485
|
-
"- WORKFRAME_ROUTING.md — lane routing\n"
|
|
486
|
-
"- WORKFRAME_KANBAN.md — execution + handoffs\n"
|
|
487
|
-
"- WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md — file conventions\n"
|
|
488
|
-
"- WORKFRAME_TELEGRAM.md — optional Telegram\n"
|
|
489
|
-
"- WORKFRAME_DISCORD.md — optional Discord\n"
|
|
490
|
-
"- WORKFRAME_AGENT_LIBRARY.md — crew model\n"
|
|
491
|
-
"- WORKFRAME_HANDOFF_SCHEMA.md — task handoff fields\n"
|
|
492
|
-
)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
def generate(
|
|
496
|
-
project_name: str,
|
|
497
|
-
pack: str,
|
|
498
|
-
output_root: Path,
|
|
499
|
-
force: bool,
|
|
500
|
-
telegram: bool,
|
|
501
|
-
discord: bool,
|
|
502
|
-
) -> Path:
|
|
503
|
-
packs = load_packs(PACKS_JSON)
|
|
504
|
-
if pack not in packs:
|
|
505
|
-
raise SystemExit(f"Unknown pack '{pack}'. Available: {', '.join(sorted(packs.keys()))}")
|
|
506
|
-
|
|
507
|
-
profiles = resolve_pack_profiles(packs[pack]['profiles'], project_name)
|
|
508
|
-
native_slug = native_profile_slug(project_name)
|
|
509
|
-
native_name = native_agent_name(project_name)
|
|
510
|
-
ctx = render_context(project_name)
|
|
511
|
-
docker = docker_container_names(project_name)
|
|
512
|
-
target = output_root / project_name
|
|
513
|
-
files_root = target / 'Files'
|
|
514
|
-
slug = slugify(project_name)
|
|
515
|
-
|
|
516
|
-
if target.exists():
|
|
517
|
-
if not force:
|
|
518
|
-
raise SystemExit(f'Target exists: {target}. Use --force to overwrite.')
|
|
519
|
-
shutil.rmtree(target)
|
|
520
|
-
|
|
521
|
-
target.mkdir(parents=True, exist_ok=True)
|
|
522
|
-
(target / 'Agents').mkdir(parents=True, exist_ok=True)
|
|
523
|
-
files_root.mkdir(parents=True, exist_ok=True)
|
|
524
|
-
(files_root / 'artifacts').mkdir(parents=True, exist_ok=True)
|
|
525
|
-
(files_root / 'docs').mkdir(parents=True, exist_ok=True)
|
|
526
|
-
(files_root / 'prompts').mkdir(parents=True, exist_ok=True)
|
|
527
|
-
(target / 'Agents' / '.gitkeep').write_text('')
|
|
528
|
-
(files_root / '.gitkeep').write_text('')
|
|
529
|
-
|
|
530
|
-
rules_agents = PKG_ROOT / 'rules' / 'AGENTS.md'
|
|
531
|
-
rules_hermes = PKG_ROOT / 'rules' / '.hermes.md'
|
|
532
|
-
(files_root / 'AGENTS.md').write_text(render_placeholders(rules_agents.read_text(), ctx))
|
|
533
|
-
(files_root / '.hermes.md').write_text(render_placeholders(rules_hermes.read_text(), ctx))
|
|
534
|
-
(files_root / 'README.md').write_text(
|
|
535
|
-
f"# {project_name}\n\nProject workspace (`/workspace` in Hermes).\n\nStart: `docs/SETUP.md`\n"
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
copy_tree(WORKSPACE_DOCS, files_root / 'docs')
|
|
539
|
-
(files_root / 'docs' / 'SETUP.md').write_text(make_setup_doc(project_name, pack, profiles, telegram, discord))
|
|
540
|
-
(files_root / 'docs' / 'INDEX.md').write_text(docs_index())
|
|
541
|
-
|
|
542
|
-
for doc in SHARED_DOCS:
|
|
543
|
-
src = PKG_ROOT / 'shared' / doc
|
|
544
|
-
if src.exists():
|
|
545
|
-
(files_root / 'docs' / doc).write_text(render_placeholders(src.read_text(), ctx))
|
|
546
|
-
|
|
547
|
-
copy_tree(PKG_ROOT / 'prompts', files_root / 'prompts')
|
|
548
|
-
|
|
549
|
-
for prof in profiles:
|
|
550
|
-
src = profile_soul_source(prof, project_name)
|
|
551
|
-
if not src.exists():
|
|
552
|
-
raise SystemExit(f'Missing profile template: {src}')
|
|
553
|
-
seed = target / 'scripts' / 'seed' / 'profiles' / prof / 'SOUL.md'
|
|
554
|
-
seed.parent.mkdir(parents=True, exist_ok=True)
|
|
555
|
-
seed.write_text(render_placeholders(src.read_text(), ctx))
|
|
556
|
-
|
|
557
|
-
(target / '.gitignore').write_text(GITIGNORE)
|
|
558
|
-
(target / '.dockerignore').write_text(DOCKERIGNORE)
|
|
559
|
-
(target / '.env.example').write_text(
|
|
560
|
-
'# Do not commit .env\n# Add provider creds at runtime via hermes auth add <provider>\n'
|
|
561
|
-
)
|
|
562
|
-
(target / 'docker-compose.yml').write_text(docker_compose_yaml(project_name, docker, native_slug, [native_slug]))
|
|
563
|
-
(target / 'README.md').write_text(
|
|
564
|
-
f"# {project_name}\n\nGenerated by create-workframe.\n\n"
|
|
565
|
-
"Quick start: see `Files/docs/SETUP.md`\n"
|
|
566
|
-
)
|
|
567
|
-
|
|
568
|
-
scripts = target / 'scripts'
|
|
569
|
-
scripts.mkdir(parents=True, exist_ok=True)
|
|
570
|
-
(scripts / 'setup.sh').write_text(setup_sh())
|
|
571
|
-
(scripts / 'setup.sh').chmod(0o755)
|
|
572
|
-
(scripts / 'setup.ps1').write_text(setup_ps1())
|
|
573
|
-
(scripts / 'bootstrap-profiles.sh').write_text(bootstrap_profiles_sh(profiles, project_name, native_slug, docker))
|
|
574
|
-
(scripts / 'bootstrap-profiles.sh').chmod(0o755)
|
|
575
|
-
(scripts / 'bootstrap-profiles.ps1').write_text(bootstrap_profiles_ps1(profiles, project_name, native_slug, docker))
|
|
576
|
-
|
|
577
|
-
wf = target / '.github' / 'workflows'
|
|
578
|
-
wf.mkdir(parents=True, exist_ok=True)
|
|
579
|
-
(wf / 'workframe-security.yml').write_text(CI_WORKFLOW)
|
|
580
|
-
|
|
581
|
-
manifest = {
|
|
582
|
-
'generator': 'workframe/scripts/create_workframe_scaffold.py',
|
|
583
|
-
'generated_at_utc': datetime.now(timezone.utc).isoformat(),
|
|
584
|
-
'project_name': project_name,
|
|
585
|
-
'project_slug': slug,
|
|
586
|
-
'pack': pack,
|
|
587
|
-
'profiles': profiles,
|
|
588
|
-
'native_agent': {
|
|
589
|
-
'display_name': native_name,
|
|
590
|
-
'profile_slug': native_slug,
|
|
591
|
-
},
|
|
592
|
-
'docker': {
|
|
593
|
-
'image': docker['image'],
|
|
594
|
-
'stack': docker['stack'],
|
|
595
|
-
'network': docker['network'],
|
|
596
|
-
'containers': {
|
|
597
|
-
'gateway': docker['gateway'],
|
|
598
|
-
'dashboard': docker['dashboard'],
|
|
599
|
-
'chat': docker['chat'],
|
|
600
|
-
'setup': docker['setup'],
|
|
601
|
-
},
|
|
602
|
-
},
|
|
603
|
-
'integrations': {'telegram': telegram, 'discord': discord},
|
|
604
|
-
'layout': {'workspace': 'Files', 'runtime': 'Agents'},
|
|
605
|
-
'security': {
|
|
606
|
-
'no_instance_data_in_template': True,
|
|
607
|
-
'runtime_state_directory': 'Agents',
|
|
608
|
-
},
|
|
609
|
-
}
|
|
610
|
-
(target / 'workframe-manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
|
611
|
-
return target
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def main() -> None:
|
|
615
|
-
ap = argparse.ArgumentParser(description='Generate minimal project workspace from Workframe package')
|
|
616
|
-
ap.add_argument('project_name', nargs='?', help='Project folder name to generate')
|
|
617
|
-
ap.add_argument('--pack', default='vanilla', help='Starter pack: vanilla/core/product/engineering/full')
|
|
618
|
-
ap.add_argument('--output', default='/workspace/generated', help='Output root directory')
|
|
619
|
-
ap.add_argument('--force', action='store_true', help='Overwrite target if exists')
|
|
620
|
-
ap.add_argument('--ci', action='store_true', help='Non-interactive strict mode')
|
|
621
|
-
ap.add_argument('--telegram', action='store_true', help='Include Telegram onboarding steps')
|
|
622
|
-
ap.add_argument('--discord', action='store_true', help='Include Discord onboarding steps')
|
|
623
|
-
ap.add_argument('--list-packs', action='store_true', help='List available packs')
|
|
624
|
-
args = ap.parse_args()
|
|
625
|
-
|
|
626
|
-
packs = load_packs(PACKS_JSON)
|
|
627
|
-
|
|
628
|
-
if args.list_packs:
|
|
629
|
-
for name, info in packs.items():
|
|
630
|
-
print(f"{name}: {info.get('description', '')}")
|
|
631
|
-
return
|
|
632
|
-
|
|
633
|
-
if not args.project_name:
|
|
634
|
-
raise SystemExit('project_name is required unless --list-packs is used')
|
|
635
|
-
|
|
636
|
-
target = generate(
|
|
637
|
-
args.project_name,
|
|
638
|
-
args.pack,
|
|
639
|
-
Path(args.output),
|
|
640
|
-
args.force,
|
|
641
|
-
args.telegram,
|
|
642
|
-
args.discord,
|
|
643
|
-
)
|
|
644
|
-
print(target)
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if __name__ == '__main__':
|
|
648
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""DEV/CI helper — NOT the canonical installer.
|
|
3
|
+
|
|
4
|
+
Canonical scaffold: bin/create-workframe.js (npm / npx create-workframe).
|
|
5
|
+
This script exists for Python-only smoke tests and security audit workflows.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
18
|
+
PKG_ROOT = SCRIPT_DIR.parent
|
|
19
|
+
PACKS_JSON = PKG_ROOT / 'shared' / 'WORKFRAME_AGENT_PACKS.json'
|
|
20
|
+
WORKSPACE_DOCS = PKG_ROOT / 'docs' / 'workspace-instructions'
|
|
21
|
+
|
|
22
|
+
PROJECT_AGENT_SLOT = 'project-agent'
|
|
23
|
+
NATIVE_SOUL_TEMPLATE = 'workframe-agent'
|
|
24
|
+
|
|
25
|
+
PROFILE_DESCRIPTIONS = {
|
|
26
|
+
'visionary': 'Clarifies product purpose, positioning, strategy, user value, and long-term alignment.',
|
|
27
|
+
'architect': 'Defines system design, technical boundaries, implementation plans, and code-review standards.',
|
|
28
|
+
'docs': 'Maintains AGENTS.md, .hermes.md, docs indexes, source-of-truth maps, and change summaries.',
|
|
29
|
+
'dev': 'Builds and modifies project files, scripts, tests, and implementation artifacts.',
|
|
30
|
+
'research': 'Performs technical research, market research, references, competitive analysis, and R&D notes.',
|
|
31
|
+
'designer': 'Handles UI direction, design docs, visual assets, image prompts, brand direction, and layout feedback.',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
SHARED_DOCS = ['WORKFRAME_AGENT_LIBRARY.md', 'WORKFRAME_HANDOFF_SCHEMA.md']
|
|
35
|
+
|
|
36
|
+
GITIGNORE = """# Runtime state: do not commit instance data
|
|
37
|
+
Agents/
|
|
38
|
+
**/Agents/
|
|
39
|
+
*.db
|
|
40
|
+
*.db-shm
|
|
41
|
+
*.db-wal
|
|
42
|
+
*.log
|
|
43
|
+
logs/
|
|
44
|
+
cache/
|
|
45
|
+
memories/
|
|
46
|
+
sessions/
|
|
47
|
+
kanban/
|
|
48
|
+
state/
|
|
49
|
+
|
|
50
|
+
# Bootstrap seed (optional cleanup after profile bootstrap)
|
|
51
|
+
scripts/seed/
|
|
52
|
+
|
|
53
|
+
# Secrets
|
|
54
|
+
.env
|
|
55
|
+
.env.local
|
|
56
|
+
.env.*.local
|
|
57
|
+
*.pem
|
|
58
|
+
*.key
|
|
59
|
+
*.p12
|
|
60
|
+
*.pfx
|
|
61
|
+
secrets/
|
|
62
|
+
|
|
63
|
+
# Build/tool noise
|
|
64
|
+
node_modules/
|
|
65
|
+
.venv/
|
|
66
|
+
__pycache__/
|
|
67
|
+
.pytest_cache/
|
|
68
|
+
.DS_Store
|
|
69
|
+
Thumbs.db
|
|
70
|
+
.vscode/
|
|
71
|
+
.idea/
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
DOCKERIGNORE = """.git
|
|
75
|
+
.gitignore
|
|
76
|
+
node_modules
|
|
77
|
+
.venv
|
|
78
|
+
__pycache__
|
|
79
|
+
.pytest_cache
|
|
80
|
+
*.pyc
|
|
81
|
+
*.pyo
|
|
82
|
+
*.db
|
|
83
|
+
*.db-shm
|
|
84
|
+
*.db-wal
|
|
85
|
+
*.log
|
|
86
|
+
.env
|
|
87
|
+
.env.*
|
|
88
|
+
Agents
|
|
89
|
+
cache
|
|
90
|
+
logs
|
|
91
|
+
memories
|
|
92
|
+
sessions
|
|
93
|
+
kanban
|
|
94
|
+
state
|
|
95
|
+
scripts/seed
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def slugify(name: str) -> str:
|
|
100
|
+
slug = re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-')
|
|
101
|
+
return slug or 'workframe'
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def native_profile_slug(project_name: str) -> str:
|
|
105
|
+
return f'{slugify(project_name)}-agent'
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def native_agent_name(project_name: str) -> str:
|
|
109
|
+
return f'{project_name} Agent'
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def render_context(project_name: str) -> dict[str, str]:
|
|
113
|
+
return {
|
|
114
|
+
'projectName': project_name,
|
|
115
|
+
'nativeProfileSlug': native_profile_slug(project_name),
|
|
116
|
+
'nativeAgentName': native_agent_name(project_name),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def render_placeholders(text: str, ctx: dict[str, str] | str) -> str:
|
|
121
|
+
context = render_context(ctx) if isinstance(ctx, str) else ctx
|
|
122
|
+
out = text
|
|
123
|
+
for key, value in context.items():
|
|
124
|
+
out = out.replace(f'{{{key}}}', value)
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def resolve_pack_profiles(base_profiles: list[str], project_name: str) -> list[str]:
|
|
129
|
+
native = native_profile_slug(project_name)
|
|
130
|
+
seen: set[str] = set()
|
|
131
|
+
resolved: list[str] = []
|
|
132
|
+
for profile in base_profiles:
|
|
133
|
+
slug = native if profile == PROJECT_AGENT_SLOT else profile
|
|
134
|
+
if slug not in seen:
|
|
135
|
+
seen.add(slug)
|
|
136
|
+
resolved.append(slug)
|
|
137
|
+
return resolved
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def profile_description(profile: str, project_name: str) -> str:
|
|
141
|
+
if profile == native_profile_slug(project_name):
|
|
142
|
+
return (
|
|
143
|
+
f'{native_agent_name(project_name)}: host, concierge, project manager, '
|
|
144
|
+
'orchestrator, and Workframe admin.'
|
|
145
|
+
)
|
|
146
|
+
return PROFILE_DESCRIPTIONS.get(profile, f'{profile} specialist profile.')
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def profile_soul_source(profile: str, project_name: str) -> Path:
|
|
150
|
+
template_dir = NATIVE_SOUL_TEMPLATE if profile == native_profile_slug(project_name) else profile
|
|
151
|
+
return PKG_ROOT / 'profiles' / template_dir / 'SOUL.md'
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def docker_container_names(project_name: str) -> dict:
|
|
155
|
+
slug = slugify(project_name)
|
|
156
|
+
stack = slug
|
|
157
|
+
return {
|
|
158
|
+
'slug': slug,
|
|
159
|
+
'stack': stack,
|
|
160
|
+
'image': 'nousresearch/hermes-agent:latest',
|
|
161
|
+
'network': f'{stack}-net',
|
|
162
|
+
'gateway': f'{stack}-gateway',
|
|
163
|
+
'dashboard': f'{stack}-dashboard',
|
|
164
|
+
'workframe_api': f'{stack}-workframe-api',
|
|
165
|
+
'workframe': f'{stack}-workframe',
|
|
166
|
+
'chat': f'{stack}-chat',
|
|
167
|
+
'setup': f'{stack}-setup',
|
|
168
|
+
'bootstrap_use': f'{stack}-bootstrap-use',
|
|
169
|
+
'bootstrap_list': f'{stack}-bootstrap-list',
|
|
170
|
+
'profile_dashboard': lambda profile: f'{stack}-dashboard-{profile}',
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def hermes_service_volumes_block() -> str:
|
|
175
|
+
return """ volumes:
|
|
176
|
+
- ./Agents:/opt/data
|
|
177
|
+
- ./Files:/workspace
|
|
178
|
+
- ./scripts:/opt/install/scripts:ro"""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def profile_dashboard_service_yaml(profile: str, docker: dict, label_project: str, network: str) -> str:
|
|
182
|
+
profile_esc = profile.replace('"', '\\"')
|
|
183
|
+
return f"""
|
|
184
|
+
dashboard-{profile}:
|
|
185
|
+
image: {docker['image']}
|
|
186
|
+
container_name: {docker['profile_dashboard'](profile)}
|
|
187
|
+
restart: unless-stopped
|
|
188
|
+
command: ["hermes", "-p", "{profile_esc}", "dashboard", "--host", "0.0.0.0", "--insecure", "--tui"]
|
|
189
|
+
labels:
|
|
190
|
+
com.workframe.project: "{label_project}"
|
|
191
|
+
com.workframe.role: profile-dashboard
|
|
192
|
+
com.workframe.profile: "{profile_esc}"
|
|
193
|
+
expose:
|
|
194
|
+
- "9119"
|
|
195
|
+
{hermes_service_volumes_block()}
|
|
196
|
+
environment:
|
|
197
|
+
- GATEWAY_HEALTH_URL=http://gateway:8642
|
|
198
|
+
- HERMES_DASHBOARD_TUI=1
|
|
199
|
+
depends_on:
|
|
200
|
+
- gateway
|
|
201
|
+
networks:
|
|
202
|
+
- {network}"""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def docker_compose_yaml(project_name: str, docker: dict, native_profile: str, _pack_profiles: list[str]) -> str:
|
|
206
|
+
label_project = project_name.replace('\\', '\\\\').replace('"', '\\"')
|
|
207
|
+
profile_esc = native_profile.replace('"', '\\"')
|
|
208
|
+
dashboard_profiles = [native_profile]
|
|
209
|
+
profile_services = ''.join(
|
|
210
|
+
profile_dashboard_service_yaml(p, docker, label_project, docker['network']) for p in dashboard_profiles
|
|
211
|
+
)
|
|
212
|
+
profile_depends = ''.join(f'\n - dashboard-{p}' for p in pack_profiles)
|
|
213
|
+
volumes = hermes_service_volumes_block()
|
|
214
|
+
return f"""name: {docker['stack']}
|
|
215
|
+
|
|
216
|
+
services:
|
|
217
|
+
gateway:
|
|
218
|
+
image: {docker['image']}
|
|
219
|
+
container_name: {docker['gateway']}
|
|
220
|
+
restart: unless-stopped
|
|
221
|
+
command: ["hermes", "-p", "{profile_esc}", "gateway", "run"]
|
|
222
|
+
labels:
|
|
223
|
+
com.workframe.project: "{label_project}"
|
|
224
|
+
com.workframe.role: gateway
|
|
225
|
+
ports:
|
|
226
|
+
- "127.0.0.1:18642:8642"
|
|
227
|
+
{volumes}
|
|
228
|
+
networks:
|
|
229
|
+
- {docker['network']}
|
|
230
|
+
|
|
231
|
+
dashboard:
|
|
232
|
+
image: {docker['image']}
|
|
233
|
+
container_name: {docker['dashboard']}
|
|
234
|
+
restart: unless-stopped
|
|
235
|
+
command: dashboard --host 0.0.0.0 --insecure --tui
|
|
236
|
+
labels:
|
|
237
|
+
com.workframe.project: "{label_project}"
|
|
238
|
+
com.workframe.role: dashboard
|
|
239
|
+
ports:
|
|
240
|
+
- "127.0.0.1:19119:9119"
|
|
241
|
+
{volumes}
|
|
242
|
+
environment:
|
|
243
|
+
- GATEWAY_HEALTH_URL=http://gateway:8642
|
|
244
|
+
- HERMES_DASHBOARD_TUI=1
|
|
245
|
+
depends_on:
|
|
246
|
+
- gateway
|
|
247
|
+
networks:
|
|
248
|
+
- {docker['network']}
|
|
249
|
+
{profile_services}
|
|
250
|
+
|
|
251
|
+
networks:
|
|
252
|
+
{docker['network']}:
|
|
253
|
+
driver: bridge
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def setup_sh() -> str:
|
|
258
|
+
return """#!/usr/bin/env bash
|
|
259
|
+
set -euo pipefail
|
|
260
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
261
|
+
cd "$ROOT"
|
|
262
|
+
|
|
263
|
+
mkdir -p Agents Files
|
|
264
|
+
|
|
265
|
+
echo "Pulling Hermes image..."
|
|
266
|
+
docker pull nousresearch/hermes-agent:latest
|
|
267
|
+
|
|
268
|
+
echo ""
|
|
269
|
+
echo "Run Hermes setup (interactive):"
|
|
270
|
+
echo " docker run --rm -it --entrypoint hermes \\\\"
|
|
271
|
+
echo " -v \\"\\$PWD/Agents:/opt/data\\" \\\\"
|
|
272
|
+
echo " -v \\"\\$PWD/Files:/workspace\\" \\\\"
|
|
273
|
+
echo " nousresearch/hermes-agent:latest setup"
|
|
274
|
+
echo ""
|
|
275
|
+
echo "Then bootstrap profiles:"
|
|
276
|
+
echo " ./scripts/bootstrap-profiles.sh"
|
|
277
|
+
echo ""
|
|
278
|
+
echo "Start gateway + dashboard:"
|
|
279
|
+
echo " docker compose up -d"
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def setup_ps1() -> str:
|
|
284
|
+
return """$ErrorActionPreference = 'Stop'
|
|
285
|
+
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|
286
|
+
Set-Location $Root
|
|
287
|
+
|
|
288
|
+
New-Item -ItemType Directory -Force -Path Agents, Files | Out-Null
|
|
289
|
+
|
|
290
|
+
Write-Host 'Pulling Hermes image...'
|
|
291
|
+
docker pull nousresearch/hermes-agent:latest
|
|
292
|
+
|
|
293
|
+
Write-Host ''
|
|
294
|
+
Write-Host 'Run Hermes setup (interactive):'
|
|
295
|
+
Write-Host ' docker run --rm -it --entrypoint hermes `'
|
|
296
|
+
Write-Host ' -v "$PWD\\Agents:/opt/data" `'
|
|
297
|
+
Write-Host ' -v "$PWD\\Files:/workspace" `'
|
|
298
|
+
Write-Host ' nousresearch/hermes-agent:latest setup'
|
|
299
|
+
Write-Host ''
|
|
300
|
+
Write-Host 'Then bootstrap profiles:'
|
|
301
|
+
Write-Host ' .\\scripts\\bootstrap-profiles.ps1'
|
|
302
|
+
Write-Host ''
|
|
303
|
+
Write-Host 'Start gateway + dashboard:'
|
|
304
|
+
Write-Host ' docker compose up -d'
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def bootstrap_profiles_sh(profiles: list[str], project_name: str, native_profile: str, docker: dict) -> str:
|
|
309
|
+
blocks = []
|
|
310
|
+
for p in profiles:
|
|
311
|
+
desc = profile_description(p, project_name).replace('"', '\\"')
|
|
312
|
+
create_name = f"{docker['stack']}-bootstrap-{p}"
|
|
313
|
+
show_name = f"{create_name}-show"
|
|
314
|
+
blocks.append(f"""echo "Creating profile: {p}"
|
|
315
|
+
docker run --rm --name "{create_name}" --entrypoint hermes \\
|
|
316
|
+
-v "$ROOT/Agents:/opt/data" \\
|
|
317
|
+
-v "$ROOT/Files:/workspace" \\
|
|
318
|
+
{docker['image']} profile create {p} --clone --description "{desc}" || true
|
|
319
|
+
|
|
320
|
+
if [ -f "$ROOT/scripts/seed/profiles/{p}/SOUL.md" ]; then
|
|
321
|
+
mkdir -p "$ROOT/Agents/profiles/{p}"
|
|
322
|
+
cp "$ROOT/scripts/seed/profiles/{p}/SOUL.md" "$ROOT/Agents/profiles/{p}/SOUL.md"
|
|
323
|
+
fi
|
|
324
|
+
""")
|
|
325
|
+
body = '\n'.join(blocks)
|
|
326
|
+
return f"""#!/usr/bin/env bash
|
|
327
|
+
set -euo pipefail
|
|
328
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
329
|
+
cd "$ROOT"
|
|
330
|
+
|
|
331
|
+
if [ ! -d "$ROOT/Agents" ]; then
|
|
332
|
+
echo "Agents/ missing. Run Hermes setup first."
|
|
333
|
+
exit 1
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
{body}
|
|
337
|
+
echo "Setting default profile to {native_profile}..."
|
|
338
|
+
docker run --rm --name "{docker['bootstrap_use']}" --entrypoint hermes \\
|
|
339
|
+
-v "$ROOT/Agents:/opt/data" \\
|
|
340
|
+
-v "$ROOT/Files:/workspace" \\
|
|
341
|
+
{docker['image']} profile use {native_profile}
|
|
342
|
+
|
|
343
|
+
echo "Profile bootstrap complete."
|
|
344
|
+
docker run --rm --name "{docker['bootstrap_list']}" --entrypoint hermes \\
|
|
345
|
+
-v "$ROOT/Agents:/opt/data" \\
|
|
346
|
+
-v "$ROOT/Files:/workspace" \\
|
|
347
|
+
{docker['image']} profile list
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def bootstrap_profiles_ps1(profiles: list[str], project_name: str, native_profile: str, docker: dict) -> str:
|
|
352
|
+
blocks = []
|
|
353
|
+
for p in profiles:
|
|
354
|
+
desc = profile_description(p, project_name).replace('"', '`"')
|
|
355
|
+
create_name = f"{docker['stack']}-bootstrap-{p}"
|
|
356
|
+
blocks.append(f"""Write-Host "Creating profile: {p}"
|
|
357
|
+
docker run --rm --name "{create_name}" --entrypoint hermes `
|
|
358
|
+
-v "$Root\\Agents:/opt/data" `
|
|
359
|
+
-v "$Root\\Files:/workspace" `
|
|
360
|
+
{docker['image']} profile create {p} --clone --description "{desc}" 2>$null
|
|
361
|
+
|
|
362
|
+
$seed = Join-Path $Root "scripts\\seed\\profiles\\{p}\\SOUL.md"
|
|
363
|
+
$destDir = Join-Path $Root "Agents\\profiles\\{p}"
|
|
364
|
+
if (Test-Path $seed) {{
|
|
365
|
+
New-Item -ItemType Directory -Force -Path $destDir | Out-Null
|
|
366
|
+
Copy-Item $seed (Join-Path $destDir "SOUL.md") -Force
|
|
367
|
+
}}
|
|
368
|
+
""")
|
|
369
|
+
body = '\n'.join(blocks)
|
|
370
|
+
return f"""$ErrorActionPreference = 'Stop'
|
|
371
|
+
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|
372
|
+
Set-Location $Root
|
|
373
|
+
|
|
374
|
+
if (-not (Test-Path "$Root\\Agents")) {{
|
|
375
|
+
throw "Agents/ missing. Run Hermes setup first."
|
|
376
|
+
}}
|
|
377
|
+
|
|
378
|
+
{body}
|
|
379
|
+
Write-Host "Setting default profile to {native_profile}..."
|
|
380
|
+
docker run --rm --name "{docker['bootstrap_use']}" --entrypoint hermes `
|
|
381
|
+
-v "$Root\\Agents:/opt/data" `
|
|
382
|
+
-v "$Root\\Files:/workspace" `
|
|
383
|
+
{docker['image']} profile use {native_profile}
|
|
384
|
+
|
|
385
|
+
Write-Host "Profile bootstrap complete."
|
|
386
|
+
docker run --rm --name "{docker['bootstrap_list']}" --entrypoint hermes `
|
|
387
|
+
-v "$Root\\Agents:/opt/data" `
|
|
388
|
+
-v "$Root\\Files:/workspace" `
|
|
389
|
+
{docker['image']} profile list
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
CI_WORKFLOW = """name: workframe-security
|
|
394
|
+
|
|
395
|
+
on:
|
|
396
|
+
push:
|
|
397
|
+
pull_request:
|
|
398
|
+
|
|
399
|
+
jobs:
|
|
400
|
+
security-and-scaffold:
|
|
401
|
+
runs-on: ubuntu-latest
|
|
402
|
+
steps:
|
|
403
|
+
- uses: actions/checkout@v4
|
|
404
|
+
- uses: actions/setup-python@v5
|
|
405
|
+
with:
|
|
406
|
+
python-version: '3.12'
|
|
407
|
+
- uses: actions/setup-node@v4
|
|
408
|
+
with:
|
|
409
|
+
node-version: '20'
|
|
410
|
+
|
|
411
|
+
- name: Run security audit
|
|
412
|
+
run: python3 scripts/security_audit.py
|
|
413
|
+
working-directory: packages/create-workframe
|
|
414
|
+
|
|
415
|
+
- name: Generate scaffold with Python
|
|
416
|
+
run: python3 scripts/create_workframe_scaffold.py CiDemo --pack core --output /tmp --force
|
|
417
|
+
working-directory: packages/create-workframe
|
|
418
|
+
|
|
419
|
+
- name: Generate scaffold with Node installer
|
|
420
|
+
run: node bin/create-workframe.js --name CiNodeDemo --pack core --out /tmp --ci --force
|
|
421
|
+
working-directory: packages/create-workframe
|
|
422
|
+
|
|
423
|
+
- name: Verify generated layout
|
|
424
|
+
run: |
|
|
425
|
+
test -f /tmp/CiDemo/Files/AGENTS.md
|
|
426
|
+
test -f /tmp/CiDemo/docker-compose.yml
|
|
427
|
+
test -f /tmp/CiNodeDemo/Files/docs/SETUP.md
|
|
428
|
+
test -f /tmp/CiNodeDemo/scripts/bootstrap-profiles.sh
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def load_packs(path: Path) -> dict:
|
|
433
|
+
data = json.loads(path.read_text())
|
|
434
|
+
packs = data.get('packs', {})
|
|
435
|
+
if not packs:
|
|
436
|
+
raise SystemExit(f'No packs found in {path}')
|
|
437
|
+
return packs
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def copy_tree(src: Path, dst: Path) -> None:
|
|
441
|
+
if not src.exists():
|
|
442
|
+
return
|
|
443
|
+
for p in src.rglob('*'):
|
|
444
|
+
if p.is_file():
|
|
445
|
+
rel = p.relative_to(src)
|
|
446
|
+
out = dst / rel
|
|
447
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
448
|
+
shutil.copy2(p, out)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def make_setup_doc(project_name: str, pack: str, profiles: list[str], telegram: bool, discord: bool) -> str:
|
|
452
|
+
tg = 'Enabled in plan (optional integration chosen).' if telegram else 'Skipped (can enable later).'
|
|
453
|
+
dc = 'Enabled in plan (optional integration chosen).' if discord else 'Skipped (can enable later).'
|
|
454
|
+
return (
|
|
455
|
+
f"# {project_name} setup\n\n"
|
|
456
|
+
"## 1) Hermes base\n"
|
|
457
|
+
"- Run `./scripts/setup.sh` or `./scripts/setup.ps1`\n"
|
|
458
|
+
"- Then Hermes setup (interactive):\n"
|
|
459
|
+
"```bash\n"
|
|
460
|
+
"docker run --rm -it --entrypoint hermes \\\n"
|
|
461
|
+
' -v "${PWD}/Agents:/opt/data" \\\n'
|
|
462
|
+
' -v "${PWD}/Files:/workspace" \\\n'
|
|
463
|
+
" nousresearch/hermes-agent:latest setup\n"
|
|
464
|
+
"```\n\n"
|
|
465
|
+
"## 2) Bootstrap agent profiles\n"
|
|
466
|
+
"- `./scripts/bootstrap-profiles.sh` or `./scripts/bootstrap-profiles.ps1`\n\n"
|
|
467
|
+
"## 3) API/model keys\n"
|
|
468
|
+
"- `hermes auth add <provider>` at runtime\n\n"
|
|
469
|
+
"## 4) Start gateway + dashboard\n"
|
|
470
|
+
"- `docker compose up -d`\n\n"
|
|
471
|
+
"## 5) Chat integrations (optional)\n"
|
|
472
|
+
f"- Telegram: {tg}\n"
|
|
473
|
+
f"- Discord: {dc}\n\n"
|
|
474
|
+
"## 6) Agent pack\n"
|
|
475
|
+
f"- Pack: **{pack}**\n"
|
|
476
|
+
f"- Profiles: {', '.join(profiles)}\n"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def docs_index() -> str:
|
|
481
|
+
return (
|
|
482
|
+
"# Docs Index\n\n"
|
|
483
|
+
"- SETUP.md — first-run onboarding\n"
|
|
484
|
+
"- WORKFRAME_ONBOARDING.md — concierge loop\n"
|
|
485
|
+
"- WORKFRAME_ROUTING.md — lane routing\n"
|
|
486
|
+
"- WORKFRAME_KANBAN.md — execution + handoffs\n"
|
|
487
|
+
"- WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md — file conventions\n"
|
|
488
|
+
"- WORKFRAME_TELEGRAM.md — optional Telegram\n"
|
|
489
|
+
"- WORKFRAME_DISCORD.md — optional Discord\n"
|
|
490
|
+
"- WORKFRAME_AGENT_LIBRARY.md — crew model\n"
|
|
491
|
+
"- WORKFRAME_HANDOFF_SCHEMA.md — task handoff fields\n"
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def generate(
|
|
496
|
+
project_name: str,
|
|
497
|
+
pack: str,
|
|
498
|
+
output_root: Path,
|
|
499
|
+
force: bool,
|
|
500
|
+
telegram: bool,
|
|
501
|
+
discord: bool,
|
|
502
|
+
) -> Path:
|
|
503
|
+
packs = load_packs(PACKS_JSON)
|
|
504
|
+
if pack not in packs:
|
|
505
|
+
raise SystemExit(f"Unknown pack '{pack}'. Available: {', '.join(sorted(packs.keys()))}")
|
|
506
|
+
|
|
507
|
+
profiles = resolve_pack_profiles(packs[pack]['profiles'], project_name)
|
|
508
|
+
native_slug = native_profile_slug(project_name)
|
|
509
|
+
native_name = native_agent_name(project_name)
|
|
510
|
+
ctx = render_context(project_name)
|
|
511
|
+
docker = docker_container_names(project_name)
|
|
512
|
+
target = output_root / project_name
|
|
513
|
+
files_root = target / 'Files'
|
|
514
|
+
slug = slugify(project_name)
|
|
515
|
+
|
|
516
|
+
if target.exists():
|
|
517
|
+
if not force:
|
|
518
|
+
raise SystemExit(f'Target exists: {target}. Use --force to overwrite.')
|
|
519
|
+
shutil.rmtree(target)
|
|
520
|
+
|
|
521
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
522
|
+
(target / 'Agents').mkdir(parents=True, exist_ok=True)
|
|
523
|
+
files_root.mkdir(parents=True, exist_ok=True)
|
|
524
|
+
(files_root / 'artifacts').mkdir(parents=True, exist_ok=True)
|
|
525
|
+
(files_root / 'docs').mkdir(parents=True, exist_ok=True)
|
|
526
|
+
(files_root / 'prompts').mkdir(parents=True, exist_ok=True)
|
|
527
|
+
(target / 'Agents' / '.gitkeep').write_text('')
|
|
528
|
+
(files_root / '.gitkeep').write_text('')
|
|
529
|
+
|
|
530
|
+
rules_agents = PKG_ROOT / 'rules' / 'AGENTS.md'
|
|
531
|
+
rules_hermes = PKG_ROOT / 'rules' / '.hermes.md'
|
|
532
|
+
(files_root / 'AGENTS.md').write_text(render_placeholders(rules_agents.read_text(), ctx))
|
|
533
|
+
(files_root / '.hermes.md').write_text(render_placeholders(rules_hermes.read_text(), ctx))
|
|
534
|
+
(files_root / 'README.md').write_text(
|
|
535
|
+
f"# {project_name}\n\nProject workspace (`/workspace` in Hermes).\n\nStart: `docs/SETUP.md`\n"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
copy_tree(WORKSPACE_DOCS, files_root / 'docs')
|
|
539
|
+
(files_root / 'docs' / 'SETUP.md').write_text(make_setup_doc(project_name, pack, profiles, telegram, discord))
|
|
540
|
+
(files_root / 'docs' / 'INDEX.md').write_text(docs_index())
|
|
541
|
+
|
|
542
|
+
for doc in SHARED_DOCS:
|
|
543
|
+
src = PKG_ROOT / 'shared' / doc
|
|
544
|
+
if src.exists():
|
|
545
|
+
(files_root / 'docs' / doc).write_text(render_placeholders(src.read_text(), ctx))
|
|
546
|
+
|
|
547
|
+
copy_tree(PKG_ROOT / 'prompts', files_root / 'prompts')
|
|
548
|
+
|
|
549
|
+
for prof in profiles:
|
|
550
|
+
src = profile_soul_source(prof, project_name)
|
|
551
|
+
if not src.exists():
|
|
552
|
+
raise SystemExit(f'Missing profile template: {src}')
|
|
553
|
+
seed = target / 'scripts' / 'seed' / 'profiles' / prof / 'SOUL.md'
|
|
554
|
+
seed.parent.mkdir(parents=True, exist_ok=True)
|
|
555
|
+
seed.write_text(render_placeholders(src.read_text(), ctx))
|
|
556
|
+
|
|
557
|
+
(target / '.gitignore').write_text(GITIGNORE)
|
|
558
|
+
(target / '.dockerignore').write_text(DOCKERIGNORE)
|
|
559
|
+
(target / '.env.example').write_text(
|
|
560
|
+
'# Do not commit .env\n# Add provider creds at runtime via hermes auth add <provider>\n'
|
|
561
|
+
)
|
|
562
|
+
(target / 'docker-compose.yml').write_text(docker_compose_yaml(project_name, docker, native_slug, [native_slug]))
|
|
563
|
+
(target / 'README.md').write_text(
|
|
564
|
+
f"# {project_name}\n\nGenerated by create-workframe.\n\n"
|
|
565
|
+
"Quick start: see `Files/docs/SETUP.md`\n"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
scripts = target / 'scripts'
|
|
569
|
+
scripts.mkdir(parents=True, exist_ok=True)
|
|
570
|
+
(scripts / 'setup.sh').write_text(setup_sh())
|
|
571
|
+
(scripts / 'setup.sh').chmod(0o755)
|
|
572
|
+
(scripts / 'setup.ps1').write_text(setup_ps1())
|
|
573
|
+
(scripts / 'bootstrap-profiles.sh').write_text(bootstrap_profiles_sh(profiles, project_name, native_slug, docker))
|
|
574
|
+
(scripts / 'bootstrap-profiles.sh').chmod(0o755)
|
|
575
|
+
(scripts / 'bootstrap-profiles.ps1').write_text(bootstrap_profiles_ps1(profiles, project_name, native_slug, docker))
|
|
576
|
+
|
|
577
|
+
wf = target / '.github' / 'workflows'
|
|
578
|
+
wf.mkdir(parents=True, exist_ok=True)
|
|
579
|
+
(wf / 'workframe-security.yml').write_text(CI_WORKFLOW)
|
|
580
|
+
|
|
581
|
+
manifest = {
|
|
582
|
+
'generator': 'workframe/scripts/create_workframe_scaffold.py',
|
|
583
|
+
'generated_at_utc': datetime.now(timezone.utc).isoformat(),
|
|
584
|
+
'project_name': project_name,
|
|
585
|
+
'project_slug': slug,
|
|
586
|
+
'pack': pack,
|
|
587
|
+
'profiles': profiles,
|
|
588
|
+
'native_agent': {
|
|
589
|
+
'display_name': native_name,
|
|
590
|
+
'profile_slug': native_slug,
|
|
591
|
+
},
|
|
592
|
+
'docker': {
|
|
593
|
+
'image': docker['image'],
|
|
594
|
+
'stack': docker['stack'],
|
|
595
|
+
'network': docker['network'],
|
|
596
|
+
'containers': {
|
|
597
|
+
'gateway': docker['gateway'],
|
|
598
|
+
'dashboard': docker['dashboard'],
|
|
599
|
+
'chat': docker['chat'],
|
|
600
|
+
'setup': docker['setup'],
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
'integrations': {'telegram': telegram, 'discord': discord},
|
|
604
|
+
'layout': {'workspace': 'Files', 'runtime': 'Agents'},
|
|
605
|
+
'security': {
|
|
606
|
+
'no_instance_data_in_template': True,
|
|
607
|
+
'runtime_state_directory': 'Agents',
|
|
608
|
+
},
|
|
609
|
+
}
|
|
610
|
+
(target / 'workframe-manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
|
611
|
+
return target
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def main() -> None:
|
|
615
|
+
ap = argparse.ArgumentParser(description='Generate minimal project workspace from Workframe package')
|
|
616
|
+
ap.add_argument('project_name', nargs='?', help='Project folder name to generate')
|
|
617
|
+
ap.add_argument('--pack', default='vanilla', help='Starter pack: vanilla/core/product/engineering/full')
|
|
618
|
+
ap.add_argument('--output', default='/workspace/generated', help='Output root directory')
|
|
619
|
+
ap.add_argument('--force', action='store_true', help='Overwrite target if exists')
|
|
620
|
+
ap.add_argument('--ci', action='store_true', help='Non-interactive strict mode')
|
|
621
|
+
ap.add_argument('--telegram', action='store_true', help='Include Telegram onboarding steps')
|
|
622
|
+
ap.add_argument('--discord', action='store_true', help='Include Discord onboarding steps')
|
|
623
|
+
ap.add_argument('--list-packs', action='store_true', help='List available packs')
|
|
624
|
+
args = ap.parse_args()
|
|
625
|
+
|
|
626
|
+
packs = load_packs(PACKS_JSON)
|
|
627
|
+
|
|
628
|
+
if args.list_packs:
|
|
629
|
+
for name, info in packs.items():
|
|
630
|
+
print(f"{name}: {info.get('description', '')}")
|
|
631
|
+
return
|
|
632
|
+
|
|
633
|
+
if not args.project_name:
|
|
634
|
+
raise SystemExit('project_name is required unless --list-packs is used')
|
|
635
|
+
|
|
636
|
+
target = generate(
|
|
637
|
+
args.project_name,
|
|
638
|
+
args.pack,
|
|
639
|
+
Path(args.output),
|
|
640
|
+
args.force,
|
|
641
|
+
args.telegram,
|
|
642
|
+
args.discord,
|
|
643
|
+
)
|
|
644
|
+
print(target)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
if __name__ == '__main__':
|
|
648
|
+
main()
|